diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2017-01-04 14:17:57 +0100 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2017-01-05 10:05:06 +0000 |
commit | 39d357e3248f80abea0159765ff39554affb40db (patch) | |
tree | aba0e6bfb76de0244bba0f5fdbd64b830dd6e621 /chromium/headless | |
parent | 87778abf5a1f89266f37d1321b92a21851d8244d (diff) | |
download | qtwebengine-chromium-39d357e3248f80abea0159765ff39554affb40db.tar.gz |
BASELINE: Update Chromium to 55.0.2883.105
And updates ninja to 1.7.2
Change-Id: I20d43c737f82764d857ada9a55586901b18b9243
Reviewed-by: Michael BrĂ¼ning <michael.bruning@qt.io>
Diffstat (limited to 'chromium/headless')
84 files changed, 5628 insertions, 691 deletions
diff --git a/chromium/headless/BUILD.gn b/chromium/headless/BUILD.gn index 27b83f03b5d..8e5970bde4a 100644 --- a/chromium/headless/BUILD.gn +++ b/chromium/headless/BUILD.gn @@ -2,6 +2,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +import("//mojo/public/tools/bindings/mojom.gni") import("//testing/test.gni") import("//tools/grit/grit_rule.gni") import("//tools/grit/repack.gni") @@ -58,6 +59,74 @@ grit("headless_lib_resources_grit") { ] } +generated_devtools_api = [ + "$target_gen_dir/public/domains/accessibility.cc", + "$target_gen_dir/public/domains/accessibility.h", + "$target_gen_dir/public/domains/animation.cc", + "$target_gen_dir/public/domains/animation.h", + "$target_gen_dir/public/domains/application_cache.cc", + "$target_gen_dir/public/domains/application_cache.h", + "$target_gen_dir/public/domains/browser.cc", + "$target_gen_dir/public/domains/browser.h", + "$target_gen_dir/public/domains/cache_storage.cc", + "$target_gen_dir/public/domains/cache_storage.h", + "$target_gen_dir/public/domains/console.cc", + "$target_gen_dir/public/domains/console.h", + "$target_gen_dir/public/domains/css.cc", + "$target_gen_dir/public/domains/css.h", + "$target_gen_dir/public/domains/database.cc", + "$target_gen_dir/public/domains/database.h", + "$target_gen_dir/public/domains/debugger.cc", + "$target_gen_dir/public/domains/debugger.h", + "$target_gen_dir/public/domains/device_orientation.cc", + "$target_gen_dir/public/domains/device_orientation.h", + "$target_gen_dir/public/domains/dom_debugger.cc", + "$target_gen_dir/public/domains/dom_debugger.h", + "$target_gen_dir/public/domains/dom.cc", + "$target_gen_dir/public/domains/dom.h", + "$target_gen_dir/public/domains/dom_storage.cc", + "$target_gen_dir/public/domains/dom_storage.h", + "$target_gen_dir/public/domains/emulation.cc", + "$target_gen_dir/public/domains/emulation.h", + "$target_gen_dir/public/domains/heap_profiler.cc", + "$target_gen_dir/public/domains/heap_profiler.h", + "$target_gen_dir/public/domains/indexeddb.cc", + "$target_gen_dir/public/domains/indexeddb.h", + "$target_gen_dir/public/domains/input.cc", + "$target_gen_dir/public/domains/input.h", + "$target_gen_dir/public/domains/inspector.cc", + "$target_gen_dir/public/domains/inspector.h", + "$target_gen_dir/public/domains/io.cc", + "$target_gen_dir/public/domains/io.h", + "$target_gen_dir/public/domains/layer_tree.cc", + "$target_gen_dir/public/domains/layer_tree.h", + "$target_gen_dir/public/domains/log.cc", + "$target_gen_dir/public/domains/log.h", + "$target_gen_dir/public/domains/memory.cc", + "$target_gen_dir/public/domains/memory.h", + "$target_gen_dir/public/domains/network.cc", + "$target_gen_dir/public/domains/network.h", + "$target_gen_dir/public/domains/page.cc", + "$target_gen_dir/public/domains/page.h", + "$target_gen_dir/public/domains/profiler.cc", + "$target_gen_dir/public/domains/profiler.h", + "$target_gen_dir/public/domains/rendering.cc", + "$target_gen_dir/public/domains/rendering.h", + "$target_gen_dir/public/domains/runtime.cc", + "$target_gen_dir/public/domains/runtime.h", + "$target_gen_dir/public/domains/security.cc", + "$target_gen_dir/public/domains/security.h", + "$target_gen_dir/public/domains/service_worker.cc", + "$target_gen_dir/public/domains/service_worker.h", + "$target_gen_dir/public/domains/tracing.cc", + "$target_gen_dir/public/domains/tracing.h", + "$target_gen_dir/public/domains/type_conversions.h", + "$target_gen_dir/public/domains/types.cc", + "$target_gen_dir/public/domains/types.h", + "$target_gen_dir/public/domains/worker.cc", + "$target_gen_dir/public/domains/worker.h", +] + action("gen_devtools_client_api") { script = "//headless/lib/browser/client_api_generator.py" deps = [ @@ -67,69 +136,7 @@ action("gen_devtools_client_api") { "$root_gen_dir/blink/core/inspector/protocol.json", ] - outputs = [ - "$target_gen_dir/public/domains/accessibility.cc", - "$target_gen_dir/public/domains/accessibility.h", - "$target_gen_dir/public/domains/animation.cc", - "$target_gen_dir/public/domains/animation.h", - "$target_gen_dir/public/domains/application_cache.cc", - "$target_gen_dir/public/domains/application_cache.h", - "$target_gen_dir/public/domains/cache_storage.cc", - "$target_gen_dir/public/domains/cache_storage.h", - "$target_gen_dir/public/domains/console.cc", - "$target_gen_dir/public/domains/console.h", - "$target_gen_dir/public/domains/css.cc", - "$target_gen_dir/public/domains/css.h", - "$target_gen_dir/public/domains/database.cc", - "$target_gen_dir/public/domains/database.h", - "$target_gen_dir/public/domains/debugger.cc", - "$target_gen_dir/public/domains/debugger.h", - "$target_gen_dir/public/domains/device_orientation.cc", - "$target_gen_dir/public/domains/device_orientation.h", - "$target_gen_dir/public/domains/dom_debugger.cc", - "$target_gen_dir/public/domains/dom_debugger.h", - "$target_gen_dir/public/domains/dom.cc", - "$target_gen_dir/public/domains/dom.h", - "$target_gen_dir/public/domains/dom_storage.cc", - "$target_gen_dir/public/domains/dom_storage.h", - "$target_gen_dir/public/domains/emulation.cc", - "$target_gen_dir/public/domains/emulation.h", - "$target_gen_dir/public/domains/heap_profiler.cc", - "$target_gen_dir/public/domains/heap_profiler.h", - "$target_gen_dir/public/domains/indexeddb.cc", - "$target_gen_dir/public/domains/indexeddb.h", - "$target_gen_dir/public/domains/input.cc", - "$target_gen_dir/public/domains/input.h", - "$target_gen_dir/public/domains/inspector.cc", - "$target_gen_dir/public/domains/inspector.h", - "$target_gen_dir/public/domains/io.cc", - "$target_gen_dir/public/domains/io.h", - "$target_gen_dir/public/domains/layer_tree.cc", - "$target_gen_dir/public/domains/layer_tree.h", - "$target_gen_dir/public/domains/memory.cc", - "$target_gen_dir/public/domains/memory.h", - "$target_gen_dir/public/domains/network.cc", - "$target_gen_dir/public/domains/network.h", - "$target_gen_dir/public/domains/page.cc", - "$target_gen_dir/public/domains/page.h", - "$target_gen_dir/public/domains/profiler.cc", - "$target_gen_dir/public/domains/profiler.h", - "$target_gen_dir/public/domains/rendering.cc", - "$target_gen_dir/public/domains/rendering.h", - "$target_gen_dir/public/domains/runtime.cc", - "$target_gen_dir/public/domains/runtime.h", - "$target_gen_dir/public/domains/security.cc", - "$target_gen_dir/public/domains/security.h", - "$target_gen_dir/public/domains/service_worker.cc", - "$target_gen_dir/public/domains/service_worker.h", - "$target_gen_dir/public/domains/tracing.cc", - "$target_gen_dir/public/domains/tracing.h", - "$target_gen_dir/public/domains/type_conversions.h", - "$target_gen_dir/public/domains/types.cc", - "$target_gen_dir/public/domains/types.h", - "$target_gen_dir/public/domains/worker.cc", - "$target_gen_dir/public/domains/worker.h", - ] + outputs = generated_devtools_api sources = [ "lib/browser/domain_cc.template", @@ -148,115 +155,82 @@ action("gen_devtools_client_api") { } static_library("headless_lib") { - sources = [ - "$target_gen_dir/public/domains/accessibility.cc", - "$target_gen_dir/public/domains/accessibility.h", - "$target_gen_dir/public/domains/animation.cc", - "$target_gen_dir/public/domains/animation.h", - "$target_gen_dir/public/domains/application_cache.cc", - "$target_gen_dir/public/domains/application_cache.h", - "$target_gen_dir/public/domains/cache_storage.cc", - "$target_gen_dir/public/domains/cache_storage.h", - "$target_gen_dir/public/domains/console.cc", - "$target_gen_dir/public/domains/console.h", - "$target_gen_dir/public/domains/css.cc", - "$target_gen_dir/public/domains/css.h", - "$target_gen_dir/public/domains/database.cc", - "$target_gen_dir/public/domains/database.h", - "$target_gen_dir/public/domains/debugger.cc", - "$target_gen_dir/public/domains/debugger.h", - "$target_gen_dir/public/domains/device_orientation.cc", - "$target_gen_dir/public/domains/device_orientation.h", - "$target_gen_dir/public/domains/dom.cc", - "$target_gen_dir/public/domains/dom.h", - "$target_gen_dir/public/domains/dom_debugger.cc", - "$target_gen_dir/public/domains/dom_debugger.h", - "$target_gen_dir/public/domains/dom_storage.cc", - "$target_gen_dir/public/domains/dom_storage.h", - "$target_gen_dir/public/domains/emulation.cc", - "$target_gen_dir/public/domains/emulation.h", - "$target_gen_dir/public/domains/heap_profiler.cc", - "$target_gen_dir/public/domains/heap_profiler.h", - "$target_gen_dir/public/domains/indexeddb.cc", - "$target_gen_dir/public/domains/indexeddb.h", - "$target_gen_dir/public/domains/input.cc", - "$target_gen_dir/public/domains/input.h", - "$target_gen_dir/public/domains/inspector.cc", - "$target_gen_dir/public/domains/inspector.h", - "$target_gen_dir/public/domains/io.cc", - "$target_gen_dir/public/domains/io.h", - "$target_gen_dir/public/domains/layer_tree.cc", - "$target_gen_dir/public/domains/layer_tree.h", - "$target_gen_dir/public/domains/memory.cc", - "$target_gen_dir/public/domains/memory.h", - "$target_gen_dir/public/domains/network.cc", - "$target_gen_dir/public/domains/network.h", - "$target_gen_dir/public/domains/page.cc", - "$target_gen_dir/public/domains/page.h", - "$target_gen_dir/public/domains/profiler.cc", - "$target_gen_dir/public/domains/profiler.h", - "$target_gen_dir/public/domains/rendering.cc", - "$target_gen_dir/public/domains/rendering.h", - "$target_gen_dir/public/domains/runtime.cc", - "$target_gen_dir/public/domains/runtime.h", - "$target_gen_dir/public/domains/security.cc", - "$target_gen_dir/public/domains/security.h", - "$target_gen_dir/public/domains/service_worker.cc", - "$target_gen_dir/public/domains/service_worker.h", - "$target_gen_dir/public/domains/tracing.cc", - "$target_gen_dir/public/domains/tracing.h", - "$target_gen_dir/public/domains/type_conversions.h", - "$target_gen_dir/public/domains/types.cc", - "$target_gen_dir/public/domains/types.h", - "$target_gen_dir/public/domains/worker.cc", - "$target_gen_dir/public/domains/worker.h", - "lib/browser/headless_browser_context_impl.cc", - "lib/browser/headless_browser_context_impl.h", - "lib/browser/headless_browser_impl.cc", - "lib/browser/headless_browser_impl.h", - "lib/browser/headless_browser_main_parts.cc", - "lib/browser/headless_browser_main_parts.h", - "lib/browser/headless_content_browser_client.cc", - "lib/browser/headless_content_browser_client.h", - "lib/browser/headless_devtools.cc", - "lib/browser/headless_devtools.h", - "lib/browser/headless_devtools_client_impl.cc", - "lib/browser/headless_devtools_client_impl.h", - "lib/browser/headless_screen.cc", - "lib/browser/headless_screen.h", - "lib/browser/headless_url_request_context_getter.cc", - "lib/browser/headless_url_request_context_getter.h", - "lib/browser/headless_web_contents_impl.cc", - "lib/browser/headless_web_contents_impl.h", - "lib/browser/headless_window_tree_client.cc", - "lib/browser/headless_window_tree_client.h", - "lib/headless_content_client.cc", - "lib/headless_content_client.h", - "lib/headless_content_main_delegate.cc", - "lib/headless_content_main_delegate.h", - "lib/renderer/headless_content_renderer_client.cc", - "lib/renderer/headless_content_renderer_client.h", - "lib/utility/headless_content_utility_client.cc", - "lib/utility/headless_content_utility_client.h", - "public/headless_browser.cc", - "public/headless_browser.h", - "public/headless_browser_context.h", - "public/headless_devtools_client.h", - "public/headless_devtools_host.h", - "public/headless_devtools_target.h", - "public/headless_export.h", - "public/headless_web_contents.h", - "public/internal/message_dispatcher.h", - "public/internal/value_conversions.h", - "public/util/error_reporter.cc", - "public/util/error_reporter.h", - ] + sources = generated_devtools_api + [ + "lib/browser/headless_browser_context_impl.cc", + "lib/browser/headless_browser_context_impl.h", + "lib/browser/headless_browser_context_options.cc", + "lib/browser/headless_browser_context_options.h", + "lib/browser/headless_browser_impl.cc", + "lib/browser/headless_browser_impl.h", + "lib/browser/headless_browser_main_parts.cc", + "lib/browser/headless_browser_main_parts.h", + "lib/browser/headless_content_browser_client.cc", + "lib/browser/headless_content_browser_client.h", + "lib/browser/headless_devtools.cc", + "lib/browser/headless_devtools.h", + "lib/browser/headless_devtools_client_impl.cc", + "lib/browser/headless_devtools_client_impl.h", + "lib/browser/headless_devtools_manager_delegate.cc", + "lib/browser/headless_devtools_manager_delegate.h", + "lib/browser/headless_screen.cc", + "lib/browser/headless_screen.h", + "lib/browser/headless_url_request_context_getter.cc", + "lib/browser/headless_url_request_context_getter.h", + "lib/browser/headless_web_contents_impl.cc", + "lib/browser/headless_web_contents_impl.h", + "lib/browser/headless_window_tree_client.cc", + "lib/browser/headless_window_tree_client.h", + "lib/headless_content_client.cc", + "lib/headless_content_client.h", + "lib/headless_content_main_delegate.cc", + "lib/headless_content_main_delegate.h", + "lib/renderer/headless_content_renderer_client.cc", + "lib/renderer/headless_content_renderer_client.h", + "lib/utility/headless_content_utility_client.cc", + "lib/utility/headless_content_utility_client.h", + "public/headless_browser.cc", + "public/headless_browser.h", + "public/headless_browser_context.h", + "public/headless_devtools_client.h", + "public/headless_devtools_host.h", + "public/headless_devtools_target.h", + "public/headless_export.h", + "public/headless_web_contents.h", + "public/internal/message_dispatcher.h", + "public/internal/value_conversions.h", + "public/util/black_hole_protocol_handler.cc", + "public/util/black_hole_protocol_handler.h", + "public/util/deterministic_dispatcher.cc", + "public/util/deterministic_dispatcher.h", + "public/util/deterministic_http_protocol_handler.cc", + "public/util/deterministic_http_protocol_handler.h", + "public/util/error_reporter.cc", + "public/util/error_reporter.h", + "public/util/expedited_dispatcher.cc", + "public/util/expedited_dispatcher.h", + "public/util/generic_url_request_job.cc", + "public/util/generic_url_request_job.h", + "public/util/http_url_fetcher.cc", + "public/util/http_url_fetcher.h", + "public/util/in_memory_protocol_handler.cc", + "public/util/in_memory_protocol_handler.h", + "public/util/in_memory_request_job.cc", + "public/util/in_memory_request_job.h", + "public/util/managed_dispatch_url_request_job.cc", + "public/util/managed_dispatch_url_request_job.h", + "public/util/testing/generic_url_request_mocks.cc", + "public/util/testing/generic_url_request_mocks.h", + "public/util/url_fetcher.cc", + "public/util/url_fetcher.h", + "public/util/url_request_dispatcher.h", + "public/util/user_agent.cc", + "public/util/user_agent.h", + ] deps = [ ":gen_devtools_client_api", ":pak", "//base", - "//components/devtools_http_handler", "//content/public/app:both", "//content/public/browser", "//content/public/child", @@ -264,6 +238,8 @@ static_library("headless_lib") { "//content/public/renderer", "//content/public/utility", "//net", + "//services/shell/public/cpp", + "//third_party/mesa:osmesa", "//ui/aura", "//ui/base", "//ui/compositor", @@ -288,13 +264,19 @@ group("headless_tests") { test("headless_unittests") { sources = [ "public/domains/types_unittest.cc", + "public/util/deterministic_dispatcher_test.cc", "public/util/error_reporter_unittest.cc", + "public/util/expedited_dispatcher_test.cc", + "public/util/generic_url_request_job_test.cc", + "public/util/testing/fake_managed_dispatch_url_request_job.cc", + "public/util/testing/fake_managed_dispatch_url_request_job.h", ] deps = [ ":headless_lib", "//base/test:run_all_unittests", "//base/test:test_support", + "//testing/gmock", "//testing/gtest", ] } @@ -316,8 +298,41 @@ action("client_api_generator_tests") { ] } +mojom("embedder_mojo_for_testing") { + sources = [ + "lib/embedder_test.mojom", + ] +} + +grit("headless_browsertest_resources_grit") { + source = "lib/headless_browsertest_resources.grd" + outputs = [ + "grit/headless_browsertest_resources.h", + "$root_gen_dir/headless/headless_browsertest_resources.pak", + ] + grit_flags = [ + "-E", + "mojom_root=" + rebase_path(root_gen_dir), + ] + deps = [ + ":embedder_mojo_for_testing__generator", + ] + resource_ids = "lib/headless_browsertest_resource_ids" +} + +repack("headless_browser_tests_pak") { + sources = [ + "$root_gen_dir/headless/headless_browsertest_resources.pak", + ] + output = "$root_out_dir/headless_browser_tests.pak" + deps = [ + ":headless_browsertest_resources_grit", + ] +} + test("headless_browsertests") { sources = [ + "lib/embedder_mojo_browsertest.cc", "lib/headless_browser_browsertest.cc", "lib/headless_browser_context_browsertest.cc", "lib/headless_devtools_client_browsertest.cc", @@ -331,11 +346,16 @@ test("headless_browsertests") { "test/test_url_request_job.h", ] + data = [ + "$root_out_dir/headless_browser_tests.pak", + ] + defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ] deps = [ + ":embedder_mojo_for_testing", + ":headless_browser_tests_pak", "//base", - "//content/test:browsertest_base", "//content/test:test_support", "//headless:headless_lib", "//testing/gmock", diff --git a/chromium/headless/DEPS b/chromium/headless/DEPS index 2e747057cbf..cc93e6b7e6c 100644 --- a/chromium/headless/DEPS +++ b/chromium/headless/DEPS @@ -1,11 +1,13 @@ include_rules = [ - "+components/devtools_http_handler", "+content/public", + "+mojo/public", "+net", "+ui/base", "+ui/base/resource", "+ui/display", "+ui/gfx", "+ui/gfx/geometry", + "+ui/gl", "+ui/ozone/public", + "+services/shell/public", ] diff --git a/chromium/headless/app/headless_shell.cc b/chromium/headless/app/headless_shell.cc index f9e8e3f45d4..c2e3690f53d 100644 --- a/chromium/headless/app/headless_shell.cc +++ b/chromium/headless/app/headless_shell.cc @@ -18,12 +18,15 @@ #include "base/strings/string_number_conversions.h" #include "content/public/common/content_switches.h" #include "headless/app/headless_shell_switches.h" +#include "headless/public/domains/emulation.h" #include "headless/public/domains/page.h" #include "headless/public/domains/runtime.h" #include "headless/public/headless_browser.h" #include "headless/public/headless_devtools_client.h" #include "headless/public/headless_devtools_target.h" #include "headless/public/headless_web_contents.h" +#include "headless/public/util/deterministic_dispatcher.h" +#include "headless/public/util/deterministic_http_protocol_handler.h" #include "net/base/file_stream.h" #include "net/base/io_buffer.h" #include "net/base/ip_address.h" @@ -31,8 +34,10 @@ #include "ui/gfx/geometry/size.h" using headless::HeadlessBrowser; +using headless::HeadlessBrowserContext; using headless::HeadlessDevToolsClient; using headless::HeadlessWebContents; +namespace emulation = headless::emulation; namespace page = headless::page; namespace runtime = headless::runtime; @@ -41,25 +46,68 @@ namespace { const char kDevToolsHttpServerAddress[] = "127.0.0.1"; // Default file name for screenshot. Can be overriden by "--screenshot" switch. const char kDefaultScreenshotFileName[] = "screenshot.png"; + +bool ParseWindowSize(std::string window_size, gfx::Size* parsed_window_size) { + int width, height = 0; + if (sscanf(window_size.c_str(), "%dx%d", &width, &height) >= 2 && + width >= 0 && height >= 0) { + parsed_window_size->set_width(width); + parsed_window_size->set_height(height); + return true; + } + return false; } +} // namespace // A sample application which demonstrates the use of the headless API. -class HeadlessShell : public HeadlessWebContents::Observer, page::Observer { +class HeadlessShell : public HeadlessWebContents::Observer, + emulation::ExperimentalObserver, + page::Observer { public: HeadlessShell() : browser_(nullptr), devtools_client_(HeadlessDevToolsClient::Create()), web_contents_(nullptr), - processed_page_ready_(false) {} + processed_page_ready_(false), + browser_context_(nullptr) {} ~HeadlessShell() override {} void OnStart(HeadlessBrowser* browser) { browser_ = browser; - HeadlessWebContents::Builder builder(browser_->CreateWebContentsBuilder()); + HeadlessBrowserContext::Builder context_builder = + browser_->CreateBrowserContextBuilder(); + if (base::CommandLine::ForCurrentProcess()->HasSwitch( + headless::switches::kDeterministicFetch)) { + deterministic_dispatcher_.reset( + new headless::DeterministicDispatcher(browser_->BrowserIOThread())); + + headless::ProtocolHandlerMap protocol_handlers; + protocol_handlers[url::kHttpScheme] = + base::MakeUnique<headless::DeterministicHttpProtocolHandler>( + deterministic_dispatcher_.get(), browser->BrowserIOThread()); + protocol_handlers[url::kHttpsScheme] = + base::MakeUnique<headless::DeterministicHttpProtocolHandler>( + deterministic_dispatcher_.get(), browser->BrowserIOThread()); + + context_builder.SetProtocolHandlers(std::move(protocol_handlers)); + } + if (base::CommandLine::ForCurrentProcess()->HasSwitch( + headless::switches::kHideScrollbars)) { + context_builder.SetOverrideWebPreferencesCallback( + base::Bind([](headless::WebPreferences* preferences) { + preferences->hide_scrollbars = true; + })); + } + browser_context_ = context_builder.Build(); + + HeadlessWebContents::Builder builder( + browser_context_->CreateWebContentsBuilder()); base::CommandLine::StringVector args = base::CommandLine::ForCurrentProcess()->GetArgs(); + // TODO(alexclarke): Should we navigate to about:blank first if using + // virtual time? if (!args.empty() && !args[0].empty()) builder.SetInitialURL(GURL(args[0])); @@ -76,11 +124,13 @@ class HeadlessShell : public HeadlessWebContents::Observer, page::Observer { if (!web_contents_) return; if (!RemoteDebuggingEnabled()) { + devtools_client_->GetEmulation()->GetExperimental()->RemoveObserver(this); devtools_client_->GetPage()->RemoveObserver(this); web_contents_->GetDevToolsTarget()->DetachClient(devtools_client_.get()); } web_contents_->RemoveObserver(this); web_contents_ = nullptr; + browser_context_->Close(); browser_->Shutdown(); } @@ -93,7 +143,26 @@ class HeadlessShell : public HeadlessWebContents::Observer, page::Observer { devtools_client_->GetPage()->Enable(); // Check if the document had already finished loading by the time we // attached. - PollReadyState(); + + devtools_client_->GetEmulation()->GetExperimental()->AddObserver(this); + + if (base::CommandLine::ForCurrentProcess()->HasSwitch( + headless::switches::kVirtualTimeBudget)) { + std::string budget_ms_ascii = + base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( + headless::switches::kVirtualTimeBudget); + int budget_ms; + CHECK(base::StringToInt(budget_ms_ascii, &budget_ms)) + << "Expected an integer value for --virtual-time-budget="; + devtools_client_->GetEmulation()->GetExperimental()->SetVirtualTimePolicy( + emulation::SetVirtualTimePolicyParams::Builder() + .SetPolicy(emulation::VirtualTimePolicy:: + PAUSE_IF_NETWORK_FETCHES_PENDING) + .SetBudget(budget_ms) + .Build()); + } else { + PollReadyState(); + } // TODO(skyostil): Implement more features to demonstrate the devtools API. } @@ -122,8 +191,18 @@ class HeadlessShell : public HeadlessWebContents::Observer, page::Observer { } } + // emulation::Observer implementation: + void OnVirtualTimeBudgetExpired( + const emulation::VirtualTimeBudgetExpiredParams& params) override { + OnPageReady(); + } + // page::Observer implementation: void OnLoadEventFired(const page::LoadEventFiredParams& params) override { + if (base::CommandLine::ForCurrentProcess()->HasSwitch( + headless::switches::kVirtualTimeBudget)) { + return; + } OnPageReady(); } @@ -156,8 +235,9 @@ class HeadlessShell : public HeadlessWebContents::Observer, page::Observer { } void OnDomFetched(std::unique_ptr<runtime::EvaluateResult> result) { - if (result->GetWasThrown()) { - LOG(ERROR) << "Failed to evaluate document.body.innerHTML"; + if (result->HasExceptionDetails()) { + LOG(ERROR) << "Failed to evaluate document.body.innerHTML: " + << result->GetExceptionDetails()->GetText(); } else { std::string dom; if (result->GetResult()->GetValue()->GetAsString(&dom)) { @@ -280,6 +360,8 @@ class HeadlessShell : public HeadlessWebContents::Observer, page::Observer { HeadlessWebContents* web_contents_; bool processed_page_ready_; std::unique_ptr<net::FileStream> screenshot_file_stream_; + HeadlessBrowserContext* browser_context_; + std::unique_ptr<headless::DeterministicDispatcher> deterministic_dispatcher_; DISALLOW_COPY_AND_ASSIGN(HeadlessShell); }; @@ -334,6 +416,28 @@ int main(int argc, const char** argv) { command_line.GetSwitchValueASCII(switches::kHostResolverRules)); } + if (command_line.HasSwitch(headless::switches::kUseGL)) { + builder.SetGLImplementation( + command_line.GetSwitchValueASCII(headless::switches::kUseGL)); + } + + if (command_line.HasSwitch(headless::switches::kUserDataDir)) { + builder.SetUserDataDir( + command_line.GetSwitchValuePath(headless::switches::kUserDataDir)); + builder.SetIncognitoMode(false); + } + + if (command_line.HasSwitch(headless::switches::kWindowSize)) { + std::string window_size = + command_line.GetSwitchValueASCII(headless::switches::kWindowSize); + gfx::Size parsed_window_size; + if (!ParseWindowSize(window_size, &parsed_window_size)) { + LOG(ERROR) << "Malformed window size"; + return EXIT_FAILURE; + } + builder.SetWindowSize(parsed_window_size); + } + return HeadlessBrowserMain( builder.Build(), base::Bind(&HeadlessShell::OnStart, base::Unretained(&shell))); diff --git a/chromium/headless/app/headless_shell_switches.cc b/chromium/headless/app/headless_shell_switches.cc index 4a62454cc46..18cf95d3e1b 100644 --- a/chromium/headless/app/headless_shell_switches.cc +++ b/chromium/headless/app/headless_shell_switches.cc @@ -7,9 +7,17 @@ namespace headless { namespace switches { +// Instructs headless_shell to cause network fetches to complete in order of +// creation. This removes a significant source of network related +// non-determinism at the cost of slower page loads. +const char kDeterministicFetch[] = "deterministic-fetch"; + // Instructs headless_shell to print document.body.innerHTML to stdout. const char kDumpDom[] = "dump-dom"; +// Hide scrollbars from screenshots. +const char kHideScrollbars[] = "hide-scrollbars"; + // Uses a specified proxy server, overrides system settings. This switch only // affects HTTP and HTTPS requests. const char kProxyServer[] = "proxy-server"; @@ -27,5 +35,23 @@ const char kRepl[] = "repl"; // Save a screenshot of the loaded page. const char kScreenshot[] = "screenshot"; +// Sets the GL implementation to use. Use a blank string to disable GL +// rendering. +const char kUseGL[] = "use-gl"; + +// Directory where the browser stores the user profile. +const char kUserDataDir[] = "user-data-dir"; + +// If set the system waits the specified number of virtual milliseconds before +// deeming the page to be ready. For determinism virtual time does not advance +// while there are pending network fetches (i.e no timers will fire). Once all +// network fetches have completed, timers fire and if the system runs out of +// virtual time is fastforwarded so the next timer fires immediatley, until the +// specified virtual time budget is exhausted. +const char kVirtualTimeBudget[] = "virtual-time-budget"; + +// Sets the initial window size. Provided as string in the format "800x600". +const char kWindowSize[] = "window-size"; + } // namespace switches } // namespace headless diff --git a/chromium/headless/app/headless_shell_switches.h b/chromium/headless/app/headless_shell_switches.h index 7f5208b473e..3806b43232d 100644 --- a/chromium/headless/app/headless_shell_switches.h +++ b/chromium/headless/app/headless_shell_switches.h @@ -7,11 +7,17 @@ namespace headless { namespace switches { +extern const char kDeterministicFetch[]; extern const char kDumpDom[]; +extern const char kHideScrollbars[]; extern const char kProxyServer[]; extern const char kRemoteDebuggingAddress[]; extern const char kRepl[]; extern const char kScreenshot[]; +extern const char kUseGL[]; +extern const char kUserDataDir[]; +extern const char kVirtualTimeBudget[]; +extern const char kWindowSize[]; } // namespace switches } // namespace headless diff --git a/chromium/headless/lib/OWNERS b/chromium/headless/lib/OWNERS new file mode 100644 index 00000000000..08850f42120 --- /dev/null +++ b/chromium/headless/lib/OWNERS @@ -0,0 +1,2 @@ +per-file *.mojom=set noparent +per-file *.mojom=file://ipc/SECURITY_OWNERS diff --git a/chromium/headless/lib/browser/client_api_generator.py b/chromium/headless/lib/browser/client_api_generator.py index 40f1206d17b..bb93ede97aa 100644 --- a/chromium/headless/lib/browser/client_api_generator.py +++ b/chromium/headless/lib/browser/client_api_generator.py @@ -59,9 +59,17 @@ def CamelCaseToHackerStyle(name): return name.lower() -def MangleEnum(value): - # Rename NULL enumeration values to avoid a clash with the actual NULL. - return 'NONE' if value == 'NULL' else value +def SanitizeLiteral(literal): + return { + # Rename null enumeration values to avoid a clash with the NULL macro. + 'null': 'none', + # Rename mathematical constants to avoid colliding with C macros. + 'Infinity': 'InfinityValue', + '-Infinity': 'NegativeInfinityValue', + 'NaN': 'NaNValue', + # Turn negative zero into a safe identifier. + '-0': 'NegativeZeroValue', + }.get(literal, literal) def InitializeJinjaEnv(cache_dir): @@ -77,7 +85,7 @@ def InitializeJinjaEnv(cache_dir): 'to_title_case': ToTitleCase, 'dash_to_camelcase': DashToCamelCase, 'camelcase_to_hacker_style': CamelCaseToHackerStyle, - 'mangle_enum': MangleEnum, + 'sanitize_literal': SanitizeLiteral, }) jinja_env.add_extension('jinja2.ext.loopcontrols') return jinja_env @@ -353,27 +361,33 @@ def SynthesizeEventTypes(json_api): domain['types'].append(event_type) -def PatchHiddenCommandsAndEvents(json_api): +def PatchExperimentalCommandsAndEvents(json_api): """ - Mark all commands and events in hidden domains as hidden and make sure hidden - commands have at least empty parameters and return values. + Mark all commands and events in experimental domains as experimental + and make sure experimental commands have at least empty parameters + and return values. """ for domain in json_api['domains']: - if domain.get('hidden', False): + if domain.get('experimental', False): for command in domain.get('commands', []): - command['hidden'] = True + command['experimental'] = True for event in domain.get('events', []): - event['hidden'] = True + event['experimental'] = True + + +def EnsureCommandsHaveParametersAndReturnTypes(json_api): + """ + Make sure all commands have at least empty parameters and return values. This + guarantees API compatibility if a previously experimental command is made + stable. + """ + for domain in json_api['domains']: for command in domain.get('commands', []): - if not command.get('hidden', False): - continue if not 'parameters' in command: command['parameters'] = [] if not 'returns' in command: command['returns'] = [] for event in domain.get('events', []): - if not event.get('hidden', False): - continue if not 'parameters' in event: event['parameters'] = [] @@ -412,7 +426,8 @@ def GenerateDomains(jinja_env, output_dirname, json_api, class_name, if __name__ == '__main__': json_api, output_dirname = ParseArguments(sys.argv[1:]) jinja_env = InitializeJinjaEnv(output_dirname) - PatchHiddenCommandsAndEvents(json_api) + PatchExperimentalCommandsAndEvents(json_api) + EnsureCommandsHaveParametersAndReturnTypes(json_api) SynthesizeCommandTypes(json_api) SynthesizeEventTypes(json_api) PatchFullQualifiedRefs(json_api) diff --git a/chromium/headless/lib/browser/client_api_generator_unittest.py b/chromium/headless/lib/browser/client_api_generator_unittest.py index 0cd7df3fd37..d9d3f81dea7 100644 --- a/chromium/headless/lib/browser/client_api_generator_unittest.py +++ b/chromium/headless/lib/browser/client_api_generator_unittest.py @@ -35,9 +35,11 @@ class ClientApiGeneratorTest(unittest.TestCase): self.assertEqual(client_api_generator.CamelCaseToHackerStyle('LoLoLoL'), 'lo_lo_lol') - def test_MangleEnum(self): - self.assertEqual(client_api_generator.MangleEnum('FOO'), 'FOO') - self.assertEqual(client_api_generator.MangleEnum('NULL'), 'NONE') + def test_SanitizeLiteralEnum(self): + self.assertEqual(client_api_generator.SanitizeLiteral('foo'), 'foo') + self.assertEqual(client_api_generator.SanitizeLiteral('null'), 'none') + self.assertEqual(client_api_generator.SanitizeLiteral('Infinity'), + 'InfinityValue') def test_PatchFullQualifiedRefs(self): json_api = { @@ -349,12 +351,12 @@ class ClientApiGeneratorTest(unittest.TestCase): types = json_api['domains'][0]['types'] self.assertListEqual(types, expected_types) - def test_PatchHiddenDomains(self): + def test_PatchExperimentalDomains(self): json_api = { 'domains': [ { 'domain': 'domain', - 'hidden': True, + 'experimental': True, 'commands': [ { 'name': 'FooCommand', @@ -368,37 +370,13 @@ class ClientApiGeneratorTest(unittest.TestCase): } ] } - expected_types = [ - { - 'type': 'object', - 'id': 'FooCommandParams', - 'description': 'Parameters for the FooCommand command.', - 'properties': [], - }, - { - 'type': 'object', - 'id': 'FooCommandResult', - 'description': 'Result for the FooCommand command.', - 'properties': [], - }, - { - 'type': 'object', - 'id': 'BarEventParams', - 'description': 'Parameters for the BarEvent event.', - 'properties': [], - } - ] - client_api_generator.PatchHiddenCommandsAndEvents(json_api) - client_api_generator.SynthesizeCommandTypes(json_api) - client_api_generator.SynthesizeEventTypes(json_api) + client_api_generator.PatchExperimentalCommandsAndEvents(json_api) for command in json_api['domains'][0]['commands']: - self.assertTrue(command['hidden']) + self.assertTrue(command['experimental']) for event in json_api['domains'][0]['events']: - self.assertTrue(command['hidden']) - types = json_api['domains'][0]['types'] - self.assertListEqual(types, expected_types) + self.assertTrue(command['experimental']) - def test_PatchHiddenCommandsAndEvents(self): + def test_EnsureCommandsHaveParametersAndReturnTypes(self): json_api = { 'domains': [ { @@ -406,13 +384,11 @@ class ClientApiGeneratorTest(unittest.TestCase): 'commands': [ { 'name': 'FooCommand', - 'hidden': True, } ], 'events': [ { 'name': 'BarEvent', - 'hidden': True, } ] } @@ -438,13 +414,9 @@ class ClientApiGeneratorTest(unittest.TestCase): 'properties': [], } ] - client_api_generator.PatchHiddenCommandsAndEvents(json_api) + client_api_generator.EnsureCommandsHaveParametersAndReturnTypes(json_api) client_api_generator.SynthesizeCommandTypes(json_api) client_api_generator.SynthesizeEventTypes(json_api) - for command in json_api['domains'][0]['commands']: - self.assertTrue(command['hidden']) - for event in json_api['domains'][0]['events']: - self.assertTrue(command['hidden']) types = json_api['domains'][0]['types'] self.assertListEqual(types, expected_types) diff --git a/chromium/headless/lib/browser/domain_cc.template b/chromium/headless/lib/browser/domain_cc.template index 118d996627e..643afe749a8 100644 --- a/chromium/headless/lib/browser/domain_cc.template +++ b/chromium/headless/lib/browser/domain_cc.template @@ -30,26 +30,15 @@ void Domain::RemoveObserver(Observer* observer) { {# Skip redirected commands. #} {% if "redirect" in command %}{% continue %}{% endif %} - {% set class_name = 'ExperimentalDomain' if command.hidden else 'Domain' %} + {% set class_name = 'ExperimentalDomain' if command.experimental else 'Domain' %} {% set method_name = command.name | to_title_case %} - {% if "parameters" in command and "returns" in command %} void {{class_name}}::{{method_name}}(std::unique_ptr<{{method_name}}Params> params, base::Callback<void(std::unique_ptr<{{method_name}}Result>)> callback) { dispatcher_->SendMessage("{{domain.domain}}.{{command.name}}", params->Serialize(), base::Bind(&Domain::Handle{{method_name}}Response, callback)); - {% elif "parameters" in command %} -void {{class_name}}::{{method_name}}(std::unique_ptr<{{method_name}}Params> params, base::Callback<void()> callback) { - dispatcher_->SendMessage("{{domain.domain}}.{{command.name}}", params->Serialize(), std::move(callback)); - {% elif "returns" in command %} -void {{class_name}}::{{method_name}}(base::Callback<void(std::unique_ptr<{{method_name}}Result>)> callback) { - dispatcher_->SendMessage("{{domain.domain}}.{{command.name}}", base::Bind(&Domain::Handle{{method_name}}Response, std::move(callback))); - {% else %} -void {{class_name}}::{{method_name}}(base::Callback<void()> callback) { - dispatcher_->SendMessage("{{domain.domain}}.{{command.name}}", std::move(callback)); - {% endif %} } {# Generate convenience methods that take the required parameters directly. #} {% if not "parameters" in command %}{% continue %}{% endif %} - {# Don't generate these for hidden commands. #} - {% if command.hidden %}{% continue %}{% endif %} + {# Don't generate these for experimental commands. #} + {% if command.experimental %}{% continue %}{% endif %} void {{class_name}}::{{method_name}}({##} {% for parameter in command.parameters -%} @@ -59,8 +48,8 @@ void {{class_name}}::{{method_name}}({##} {% if not loop.first %}, {% endif %} {{resolve_type(parameter).pass_type}} {{parameter.name | camelcase_to_hacker_style -}} {% endfor %} - {% if "parameters" in command and not command.parameters[0].get("optional", False) %}, {% endif %}{# -#} - {% if "returns" in command -%} + {% if command.get("parameters", []) and not command.parameters[0].get("optional", False) %}, {% endif %}{# -#} + {% if command.get("returns", []) -%} base::Callback<void(std::unique_ptr<{{method_name}}Result>)> callback{##} {% else -%} base::Callback<void()> callback{##} @@ -75,8 +64,19 @@ void {{class_name}}::{{method_name}}({##} {% endfor %} .Build(); {# Send the message. #} - {{method_name}}(std::move(params), std::move(callback)); + {% if command.get("returns", []) -%} + dispatcher_->SendMessage("{{domain.domain}}.{{command.name}}", params->Serialize(), base::Bind(&Domain::Handle{{method_name}}Response, callback)); + {% else %} + dispatcher_->SendMessage("{{domain.domain}}.{{command.name}}", params->Serialize(), std::move(callback)); + {% endif %} +} + {# If the command has no return value, generate a convenience method that #} + {# accepts a base::Callback<void()> together with the parameters object. #} + {% if not command.get("returns", []) %} +void {{class_name}}::{{method_name}}(std::unique_ptr<{{method_name}}Params> params, base::Callback<void()> callback) { + dispatcher_->SendMessage("{{domain.domain}}.{{command.name}}", params->Serialize(), std::move(callback)); } + {% endif %} {% endfor %} {# Generate response handlers for commands that need them. #} diff --git a/chromium/headless/lib/browser/domain_h.template b/chromium/headless/lib/browser/domain_h.template index 607b5be16f2..2d146a70b52 100644 --- a/chromium/headless/lib/browser/domain_h.template +++ b/chromium/headless/lib/browser/domain_h.template @@ -20,18 +20,10 @@ {% if command.description %} // {{ command.description }} {% endif %} - {% if "parameters" in command and "returns" in command %} void {{method_name}}(std::unique_ptr<{{method_name}}Params> params, base::Callback<void(std::unique_ptr<{{method_name}}Result>)> callback = base::Callback<void(std::unique_ptr<{{method_name}}Result>)>()); - {% elif "parameters" in command %} - void {{method_name}}(std::unique_ptr<{{method_name}}Params> params, base::Callback<void()> callback = base::Callback<void()>()); - {% elif "returns" in command %} - void {{method_name}}(base::Callback<void(std::unique_ptr<{{method_name}}Result>)> callback = base::Callback<void(std::unique_ptr<{{method_name}}Result>)>()); - {% else %} - void {{method_name}}(base::Callback<void()> callback = base::Callback<void()>()); - {% endif %} {# Generate convenience methods that take the required parameters directly. #} - {# Don't generate these for hidden commands. #} - {% if "parameters" in command and not command.hidden %} + {# Don't generate these for experimental commands. #} + {% if "parameters" in command and not command.experimental %} void {{method_name}}({##} {% for parameter in command.parameters -%} {% if parameter.get("optional", False) -%} @@ -40,12 +32,17 @@ {% if not loop.first %}, {% endif %} {{resolve_type(parameter).pass_type}} {{parameter.name | camelcase_to_hacker_style -}} {% endfor %} - {% if "parameters" in command and not command.parameters[0].get("optional", False) %}, {% endif %}{# -#} - {% if "returns" in command -%} + {% if command.get("parameters", []) and not command.parameters[0].get("optional", False) %}, {% endif %}{# -#} + {% if command.get("returns", []) -%} base::Callback<void(std::unique_ptr<{{method_name}}Result>)> callback = base::Callback<void(std::unique_ptr<{{method_name}}Result>)>(){##} {% else -%} base::Callback<void()> callback = base::Callback<void()>(){##} {% endif %}); + {# If the command has no return value, generate a convenience method that #} + {# accepts a base::Callback<void()> together with the parameters object. #} + {% if not command.get("returns", []) %} + void {{method_name}}(std::unique_ptr<{{method_name}}Params> params, base::Callback<void()> callback); + {% endif %} {% endif %} {% endmacro %} @@ -71,9 +68,9 @@ class HEADLESS_EXPORT Observer : public ExperimentalObserver { virtual ~Observer() {} {% for event in domain.events %} {% if event.description %} - // {% if event.hidden %}Experimental: {% endif %}{{event.description}} + // {% if event.experimental %}Experimental: {% endif %}{{event.description}} {% endif %} - virtual void On{{event.name | to_title_case}}(const {{event.name | to_title_case}}Params& params) {% if event.hidden %}final {% endif %}{} + virtual void On{{event.name | to_title_case}}(const {{event.name | to_title_case}}Params& params) {% if event.experimental %}final {% endif %}{} {% endfor %} }; {% endif %} @@ -98,8 +95,8 @@ class HEADLESS_EXPORT Domain { {% for command in domain.commands %} {# Skip redirected commands. #} {% if "redirect" in command %}{% continue %}{% endif %} - {# Skip hidden commands. #} - {% if command.hidden %}{% continue %}{% endif %} + {# Skip experimental commands. #} + {% if command.experimental %}{% continue %}{% endif %} {{ command_decl(command) }} {% endfor %} protected: @@ -143,8 +140,8 @@ class ExperimentalDomain : public Domain { {% for command in domain.commands %} {# Skip redirected commands. #} {% if "redirect" in command %}{% continue %}{% endif %} - {# Skip non-hidden commands. #} - {% if not command.hidden %}{% continue %}{% endif %} + {# Skip non-experimental commands. #} + {% if not command.experimental %}{% continue %}{% endif %} {{ command_decl(command) }} {% endfor %} diff --git a/chromium/headless/lib/browser/headless_browser_context_impl.cc b/chromium/headless/lib/browser/headless_browser_context_impl.cc index ed0188050ec..df65523f34b 100644 --- a/chromium/headless/lib/browser/headless_browser_context_impl.cc +++ b/chromium/headless/lib/browser/headless_browser_context_impl.cc @@ -5,18 +5,30 @@ #include "headless/lib/browser/headless_browser_context_impl.h" #include <memory> +#include <string> +#include <utility> +#include <vector> +#include "base/guid.h" #include "base/memory/ptr_util.h" #include "base/path_service.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/resource_context.h" #include "content/public/browser/storage_partition.h" +#include "headless/lib/browser/headless_browser_context_options.h" #include "headless/lib/browser/headless_browser_impl.h" #include "headless/lib/browser/headless_url_request_context_getter.h" +#include "headless/public/util/black_hole_protocol_handler.h" +#include "headless/public/util/in_memory_protocol_handler.h" #include "net/url_request/url_request_context.h" +#include "ui/aura/window_tree_host.h" namespace headless { +namespace { +const char kHeadlessMojomProtocol[] = "headless-mojom"; +} + // Contains net::URLRequestContextGetter required for resource loading. // Must be destructed on the IO thread as per content::ResourceContext // requirements. @@ -67,15 +79,23 @@ net::URLRequestContext* HeadlessResourceContext::GetRequestContext() { } HeadlessBrowserContextImpl::HeadlessBrowserContextImpl( - ProtocolHandlerMap protocol_handlers, - HeadlessBrowser::Options* options) - : protocol_handlers_(std::move(protocol_handlers)), - options_(options), - resource_context_(new HeadlessResourceContext) { + HeadlessBrowserImpl* browser, + std::unique_ptr<HeadlessBrowserContextOptions> context_options) + : browser_(browser), + context_options_(std::move(context_options)), + resource_context_(new HeadlessResourceContext), + id_(base::GenerateGUID()) { InitWhileIOAllowed(); } HeadlessBrowserContextImpl::~HeadlessBrowserContextImpl() { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + // Destroy all web contents before shutting down storage partitions. + web_contents_map_.clear(); + + ShutdownStoragePartitions(); + if (resource_context_) { content::BrowserThread::DeleteSoon(content::BrowserThread::IO, FROM_HERE, resource_context_.release()); @@ -85,12 +105,53 @@ HeadlessBrowserContextImpl::~HeadlessBrowserContextImpl() { // static HeadlessBrowserContextImpl* HeadlessBrowserContextImpl::From( HeadlessBrowserContext* browser_context) { - return reinterpret_cast<HeadlessBrowserContextImpl*>(browser_context); + return static_cast<HeadlessBrowserContextImpl*>(browser_context); +} + +// static +HeadlessBrowserContextImpl* HeadlessBrowserContextImpl::From( + content::BrowserContext* browser_context) { + return static_cast<HeadlessBrowserContextImpl*>(browser_context); +} + +// static +std::unique_ptr<HeadlessBrowserContextImpl> HeadlessBrowserContextImpl::Create( + HeadlessBrowserContext::Builder* builder) { + return base::WrapUnique(new HeadlessBrowserContextImpl( + builder->browser_, std::move(builder->options_))); +} + +HeadlessWebContents::Builder +HeadlessBrowserContextImpl::CreateWebContentsBuilder() { + DCHECK(browser_->BrowserMainThread()->BelongsToCurrentThread()); + return HeadlessWebContents::Builder(this); +} + +std::vector<HeadlessWebContents*> +HeadlessBrowserContextImpl::GetAllWebContents() { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + std::vector<HeadlessWebContents*> result; + result.reserve(web_contents_map_.size()); + + for (const auto& web_contents_pair : web_contents_map_) { + result.push_back(web_contents_pair.second.get()); + } + + return result; +} + +void HeadlessBrowserContextImpl::Close() { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + browser_->DestroyBrowserContext(this); } void HeadlessBrowserContextImpl::InitWhileIOAllowed() { - // TODO(skyostil): Allow the embedder to override this. - PathService::Get(base::DIR_EXE, &path_); + if (!context_options_->user_data_dir().empty()) { + path_ = context_options_->user_data_dir(); + } else { + PathService::Get(base::DIR_EXE, &path_); + } BrowserContext::Initialize(this, path_); } @@ -105,7 +166,7 @@ base::FilePath HeadlessBrowserContextImpl::GetPath() const { } bool HeadlessBrowserContextImpl::IsOffTheRecord() const { - return false; + return context_options_->incognito_mode(); } content::ResourceContext* HeadlessBrowserContextImpl::GetResourceContext() { @@ -152,12 +213,12 @@ net::URLRequestContextGetter* HeadlessBrowserContextImpl::CreateRequestContext( content::URLRequestInterceptorScopedVector request_interceptors) { scoped_refptr<HeadlessURLRequestContextGetter> url_request_context_getter( new HeadlessURLRequestContextGetter( - content::BrowserThread::GetMessageLoopProxyForThread( + content::BrowserThread::GetTaskRunnerForThread( content::BrowserThread::IO), - content::BrowserThread::GetMessageLoopProxyForThread( + content::BrowserThread::GetTaskRunnerForThread( content::BrowserThread::FILE), - protocol_handlers, std::move(protocol_handlers_), - std::move(request_interceptors), options())); + protocol_handlers, context_options_->TakeProtocolHandlers(), + std::move(request_interceptors), context_options_.get())); resource_context_->set_url_request_context_getter(url_request_context_getter); return url_request_context_getter.get(); } @@ -183,13 +244,65 @@ HeadlessBrowserContextImpl::CreateMediaRequestContextForStoragePartition( return nullptr; } -void HeadlessBrowserContextImpl::SetOptionsForTesting( - HeadlessBrowser::Options* options) { - options_ = options; +HeadlessWebContents* HeadlessBrowserContextImpl::CreateWebContents( + HeadlessWebContents::Builder* builder) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + std::unique_ptr<HeadlessWebContentsImpl> headless_web_contents = + HeadlessWebContentsImpl::Create(builder, + browser()->window_tree_host()->window()); + + if (!headless_web_contents) { + return nullptr; + } + + HeadlessWebContents* result = headless_web_contents.get(); + + RegisterWebContents(std::move(headless_web_contents)); + + return result; +} + +void HeadlessBrowserContextImpl::RegisterWebContents( + std::unique_ptr<HeadlessWebContentsImpl> web_contents) { + DCHECK(web_contents); + web_contents_map_[web_contents->GetDevToolsAgentHostId()] = + std::move(web_contents); +} + +void HeadlessBrowserContextImpl::DestroyWebContents( + HeadlessWebContentsImpl* web_contents) { + auto it = web_contents_map_.find(web_contents->GetDevToolsAgentHostId()); + DCHECK(it != web_contents_map_.end()); + web_contents_map_.erase(it); +} + +HeadlessWebContents* +HeadlessBrowserContextImpl::GetWebContentsForDevToolsAgentHostId( + const std::string& devtools_agent_host_id) { + auto find_it = web_contents_map_.find(devtools_agent_host_id); + if (find_it == web_contents_map_.end()) + return nullptr; + return find_it->second.get(); +} + +HeadlessBrowserImpl* HeadlessBrowserContextImpl::browser() const { + return browser_; +} + +const HeadlessBrowserContextOptions* HeadlessBrowserContextImpl::options() + const { + return context_options_.get(); +} + +const std::string& HeadlessBrowserContextImpl::Id() const { + return id_; } HeadlessBrowserContext::Builder::Builder(HeadlessBrowserImpl* browser) - : browser_(browser) {} + : browser_(browser), + options_(new HeadlessBrowserContextOptions(browser->options())), + enable_http_and_https_if_mojo_used_(false) {} HeadlessBrowserContext::Builder::~Builder() = default; @@ -198,14 +311,106 @@ HeadlessBrowserContext::Builder::Builder(Builder&&) = default; HeadlessBrowserContext::Builder& HeadlessBrowserContext::Builder::SetProtocolHandlers( ProtocolHandlerMap protocol_handlers) { - protocol_handlers_ = std::move(protocol_handlers); + options_->protocol_handlers_ = std::move(protocol_handlers); return *this; } -std::unique_ptr<HeadlessBrowserContext> -HeadlessBrowserContext::Builder::Build() { - return base::WrapUnique(new HeadlessBrowserContextImpl( - std::move(protocol_handlers_), browser_->options())); +HeadlessBrowserContext::Builder& HeadlessBrowserContext::Builder::SetUserAgent( + const std::string& user_agent) { + options_->user_agent_ = user_agent; + return *this; +} + +HeadlessBrowserContext::Builder& +HeadlessBrowserContext::Builder::SetProxyServer( + const net::HostPortPair& proxy_server) { + options_->proxy_server_ = proxy_server; + return *this; +} + +HeadlessBrowserContext::Builder& +HeadlessBrowserContext::Builder::SetHostResolverRules( + const std::string& host_resolver_rules) { + options_->host_resolver_rules_ = host_resolver_rules; + return *this; } +HeadlessBrowserContext::Builder& HeadlessBrowserContext::Builder::SetWindowSize( + const gfx::Size& window_size) { + options_->window_size_ = window_size; + return *this; +} + +HeadlessBrowserContext::Builder& +HeadlessBrowserContext::Builder::SetUserDataDir( + const base::FilePath& user_data_dir) { + options_->user_data_dir_ = user_data_dir; + return *this; +} + +HeadlessBrowserContext::Builder& +HeadlessBrowserContext::Builder::SetIncognitoMode(bool incognito_mode) { + options_->incognito_mode_ = incognito_mode; + return *this; +} + +HeadlessBrowserContext::Builder& +HeadlessBrowserContext::Builder::AddJsMojoBindings( + const std::string& mojom_name, + const std::string& js_bindings) { + mojo_bindings_.emplace_back(mojom_name, js_bindings); + return *this; +} + +HeadlessBrowserContext::Builder& +HeadlessBrowserContext::Builder::EnableUnsafeNetworkAccessWithMojoBindings( + bool enable_http_and_https_if_mojo_used) { + enable_http_and_https_if_mojo_used_ = enable_http_and_https_if_mojo_used; + return *this; +} + +HeadlessBrowserContext::Builder& +HeadlessBrowserContext::Builder::SetOverrideWebPreferencesCallback( + base::Callback<void(WebPreferences*)> callback) { + options_->override_web_preferences_callback_ = std::move(callback); + return *this; +} + +HeadlessBrowserContext* HeadlessBrowserContext::Builder::Build() { + if (!mojo_bindings_.empty()) { + std::unique_ptr<InMemoryProtocolHandler> headless_mojom_protocol_handler( + new InMemoryProtocolHandler()); + for (const MojoBindings& binding : mojo_bindings_) { + headless_mojom_protocol_handler->InsertResponse( + binding.mojom_name, + InMemoryProtocolHandler::Response(binding.js_bindings, + "application/javascript")); + } + DCHECK(options_->protocol_handlers_.find(kHeadlessMojomProtocol) == + options_->protocol_handlers_.end()); + options_->protocol_handlers_[kHeadlessMojomProtocol] = + std::move(headless_mojom_protocol_handler); + + // Unless you know what you're doing it's unsafe to allow http/https for a + // context with mojo bindings. + if (!enable_http_and_https_if_mojo_used_) { + options_->protocol_handlers_[url::kHttpScheme] = + base::MakeUnique<BlackHoleProtocolHandler>(); + options_->protocol_handlers_[url::kHttpsScheme] = + base::MakeUnique<BlackHoleProtocolHandler>(); + } + } + + return browser_->CreateBrowserContext(this); +} + +HeadlessBrowserContext::Builder::MojoBindings::MojoBindings() {} + +HeadlessBrowserContext::Builder::MojoBindings::MojoBindings( + const std::string& mojom_name, + const std::string& js_bindings) + : mojom_name(mojom_name), js_bindings(js_bindings) {} + +HeadlessBrowserContext::Builder::MojoBindings::~MojoBindings() {} + } // namespace headless diff --git a/chromium/headless/lib/browser/headless_browser_context_impl.h b/chromium/headless/lib/browser/headless_browser_context_impl.h index fae8f8a8633..8d39dfcb44b 100644 --- a/chromium/headless/lib/browser/headless_browser_context_impl.h +++ b/chromium/headless/lib/browser/headless_browser_context_impl.h @@ -6,27 +6,44 @@ #define HEADLESS_LIB_BROWSER_HEADLESS_BROWSER_CONTEXT_IMPL_H_ #include <memory> +#include <string> +#include <unordered_map> +#include <vector> #include "base/files/file_path.h" #include "content/public/browser/browser_context.h" #include "content/public/browser/content_browser_client.h" #include "content/public/browser/resource_context.h" +#include "headless/lib/browser/headless_browser_context_options.h" #include "headless/lib/browser/headless_url_request_context_getter.h" #include "headless/public/headless_browser.h" #include "headless/public/headless_browser_context.h" namespace headless { +class HeadlessBrowserImpl; class HeadlessResourceContext; +class HeadlessWebContentsImpl; class HeadlessBrowserContextImpl : public HeadlessBrowserContext, public content::BrowserContext { public: - explicit HeadlessBrowserContextImpl(ProtocolHandlerMap protocol_handlers, - HeadlessBrowser::Options* options); ~HeadlessBrowserContextImpl() override; static HeadlessBrowserContextImpl* From( HeadlessBrowserContext* browser_context); + static HeadlessBrowserContextImpl* From( + content::BrowserContext* browser_context); + + static std::unique_ptr<HeadlessBrowserContextImpl> Create( + HeadlessBrowserContext::Builder* builder); + + // HeadlessBrowserContext implementation: + HeadlessWebContents::Builder CreateWebContentsBuilder() override; + std::vector<HeadlessWebContents*> GetAllWebContents() override; + HeadlessWebContents* GetWebContentsForDevToolsAgentHostId( + const std::string& devtools_agent_host_id) override; + void Close() override; + const std::string& Id() const override; // BrowserContext implementation: std::unique_ptr<content::ZoomLevelDelegate> CreateZoomLevelDelegate( @@ -54,19 +71,35 @@ class HeadlessBrowserContextImpl : public HeadlessBrowserContext, const base::FilePath& partition_path, bool in_memory) override; - HeadlessBrowser::Options* options() const { return options_; } - void SetOptionsForTesting(HeadlessBrowser::Options* options); + HeadlessWebContents* CreateWebContents(HeadlessWebContents::Builder* builder); + // Register web contents which were created not through Headless API + // (calling window.open() is a best example for this). + void RegisterWebContents( + std::unique_ptr<HeadlessWebContentsImpl> web_contents); + void DestroyWebContents(HeadlessWebContentsImpl* web_contents); + + HeadlessBrowserImpl* browser() const; + const HeadlessBrowserContextOptions* options() const; private: + HeadlessBrowserContextImpl( + HeadlessBrowserImpl* browser, + std::unique_ptr<HeadlessBrowserContextOptions> context_options); + // Performs initialization of the HeadlessBrowserContextImpl while IO is still // allowed on the current thread. void InitWhileIOAllowed(); - ProtocolHandlerMap protocol_handlers_; - HeadlessBrowser::Options* options_; // Not owned. + HeadlessBrowserImpl* browser_; // Not owned. + std::unique_ptr<HeadlessBrowserContextOptions> context_options_; std::unique_ptr<HeadlessResourceContext> resource_context_; base::FilePath path_; + std::unordered_map<std::string, std::unique_ptr<HeadlessWebContents>> + web_contents_map_; + + std::string id_; + DISALLOW_COPY_AND_ASSIGN(HeadlessBrowserContextImpl); }; diff --git a/chromium/headless/lib/browser/headless_browser_context_options.cc b/chromium/headless/lib/browser/headless_browser_context_options.cc new file mode 100644 index 00000000000..13faa83889e --- /dev/null +++ b/chromium/headless/lib/browser/headless_browser_context_options.cc @@ -0,0 +1,78 @@ +// 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 "headless/lib/browser/headless_browser_context_options.h" + +#include <string> +#include <utility> + +namespace headless { + +namespace { + +template <class T> +const T& ReturnOverriddenValue(const base::Optional<T>& value, + const T& default_value) { + if (value) { + return *value; + } else { + return default_value; + } +} + +} // namespace + +HeadlessBrowserContextOptions::HeadlessBrowserContextOptions( + HeadlessBrowserContextOptions&& options) = default; + +HeadlessBrowserContextOptions::~HeadlessBrowserContextOptions() = default; + +HeadlessBrowserContextOptions& HeadlessBrowserContextOptions::operator=( + HeadlessBrowserContextOptions&& options) = default; + +HeadlessBrowserContextOptions::HeadlessBrowserContextOptions( + HeadlessBrowser::Options* options) + : browser_options_(options) {} + +const std::string& HeadlessBrowserContextOptions::user_agent() const { + return ReturnOverriddenValue(user_agent_, browser_options_->user_agent); +} + +const net::HostPortPair& HeadlessBrowserContextOptions::proxy_server() const { + return ReturnOverriddenValue(proxy_server_, browser_options_->proxy_server); +} + +const std::string& HeadlessBrowserContextOptions::host_resolver_rules() const { + return ReturnOverriddenValue(host_resolver_rules_, + browser_options_->host_resolver_rules); +} + +const gfx::Size& HeadlessBrowserContextOptions::window_size() const { + return ReturnOverriddenValue(window_size_, browser_options_->window_size); +} + +const base::FilePath& HeadlessBrowserContextOptions::user_data_dir() const { + return ReturnOverriddenValue(user_data_dir_, browser_options_->user_data_dir); +} + +bool HeadlessBrowserContextOptions::incognito_mode() const { + return ReturnOverriddenValue(incognito_mode_, + browser_options_->incognito_mode); +} + +const ProtocolHandlerMap& HeadlessBrowserContextOptions::protocol_handlers() + const { + return protocol_handlers_; +} + +ProtocolHandlerMap HeadlessBrowserContextOptions::TakeProtocolHandlers() { + return std::move(protocol_handlers_); +} + +const base::Callback<void(WebPreferences*)>& +HeadlessBrowserContextOptions::override_web_preferences_callback() const { + return override_web_preferences_callback_; +} + +} // namespace headless diff --git a/chromium/headless/lib/browser/headless_browser_context_options.h b/chromium/headless/lib/browser/headless_browser_context_options.h new file mode 100644 index 00000000000..4c6601757f4 --- /dev/null +++ b/chromium/headless/lib/browser/headless_browser_context_options.h @@ -0,0 +1,78 @@ +// 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 HEADLESS_LIB_BROWSER_HEADLESS_BROWSER_CONTEXT_OPTIONS_H_ +#define HEADLESS_LIB_BROWSER_HEADLESS_BROWSER_CONTEXT_OPTIONS_H_ + +#include <string> + +#include "base/callback.h" +#include "base/files/file_path.h" +#include "base/optional.h" +#include "headless/public/headless_browser.h" +#include "headless/public/headless_browser_context.h" + +namespace headless { + +// Represents options which can be customized for a given BrowserContext. +// Provides a fallback to default browser-side options when an option +// is not set for a particular BrowserContext. +class HeadlessBrowserContextOptions { + public: + HeadlessBrowserContextOptions(HeadlessBrowserContextOptions&& options); + ~HeadlessBrowserContextOptions(); + + HeadlessBrowserContextOptions& operator=( + HeadlessBrowserContextOptions&& options); + + const std::string& user_agent() const; + + // See HeadlessBrowser::Options::proxy_server. + const net::HostPortPair& proxy_server() const; + + // See HeadlessBrowser::Options::host_resolver_rules. + const std::string& host_resolver_rules() const; + + const gfx::Size& window_size() const; + + // See HeadlessBrowser::Options::user_data_dir. + const base::FilePath& user_data_dir() const; + + // Set HeadlessBrowser::Options::incognito_mode. + bool incognito_mode() const; + + // Custom network protocol handlers. These can be used to override URL + // fetching for different network schemes. + const ProtocolHandlerMap& protocol_handlers() const; + // Since ProtocolHandlerMap is move-only, this method takes ownership of them. + ProtocolHandlerMap TakeProtocolHandlers(); + + // Callback that is invoked to override WebPreferences for RenderViews + // created within this HeadlessBrowserContext. + const base::Callback<void(WebPreferences*)>& + override_web_preferences_callback() const; + + private: + friend class HeadlessBrowserContext::Builder; + + explicit HeadlessBrowserContextOptions(HeadlessBrowser::Options*); + + HeadlessBrowser::Options* browser_options_; + + base::Optional<std::string> user_agent_; + base::Optional<net::HostPortPair> proxy_server_; + base::Optional<std::string> host_resolver_rules_; + base::Optional<gfx::Size> window_size_; + base::Optional<base::FilePath> user_data_dir_; + base::Optional<bool> incognito_mode_; + + ProtocolHandlerMap protocol_handlers_; + base::Callback<void(WebPreferences*)> override_web_preferences_callback_; + + DISALLOW_COPY_AND_ASSIGN(HeadlessBrowserContextOptions); +}; + +} // namespace headless + +#endif // HEADLESS_LIB_BROWSER_HEADLESS_BROWSER_CONTEXT_OPTIONS_H_ diff --git a/chromium/headless/lib/browser/headless_browser_impl.cc b/chromium/headless/lib/browser/headless_browser_impl.cc index a3eb1ba3740..98fcac84ed3 100644 --- a/chromium/headless/lib/browser/headless_browser_impl.cc +++ b/chromium/headless/lib/browser/headless_browser_impl.cc @@ -4,6 +4,8 @@ #include "headless/lib/browser/headless_browser_impl.h" +#include <string> +#include <utility> #include <vector> #include "base/command_line.h" @@ -49,73 +51,63 @@ HeadlessBrowserImpl::HeadlessBrowserImpl( HeadlessBrowser::Options options) : on_start_callback_(on_start_callback), options_(std::move(options)), - browser_main_parts_(nullptr) { -} + browser_main_parts_(nullptr), + weak_ptr_factory_(this) {} HeadlessBrowserImpl::~HeadlessBrowserImpl() {} -HeadlessWebContents::Builder HeadlessBrowserImpl::CreateWebContentsBuilder() { - DCHECK(BrowserMainThread()->BelongsToCurrentThread()); - return HeadlessWebContents::Builder(this); -} - HeadlessBrowserContext::Builder HeadlessBrowserImpl::CreateBrowserContextBuilder() { - DCHECK(BrowserMainThread()->BelongsToCurrentThread()); + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); return HeadlessBrowserContext::Builder(this); } -HeadlessWebContents* HeadlessBrowserImpl::CreateWebContents( - HeadlessWebContents::Builder* builder) { - DCHECK(BrowserMainThread()->BelongsToCurrentThread()); - std::unique_ptr<HeadlessWebContentsImpl> headless_web_contents = - HeadlessWebContentsImpl::Create(builder, window_tree_host_->window(), - this); - if (!headless_web_contents) - return nullptr; - return RegisterWebContents(std::move(headless_web_contents)); +scoped_refptr<base::SingleThreadTaskRunner> +HeadlessBrowserImpl::BrowserFileThread() const { + return content::BrowserThread::GetTaskRunnerForThread( + content::BrowserThread::FILE); } -HeadlessWebContents* HeadlessBrowserImpl::CreateWebContents( - const GURL& initial_url, - const gfx::Size& size) { - return CreateWebContentsBuilder() - .SetInitialURL(initial_url) - .SetWindowSize(size) - .Build(); +scoped_refptr<base::SingleThreadTaskRunner> +HeadlessBrowserImpl::BrowserIOThread() const { + return content::BrowserThread::GetTaskRunnerForThread( + content::BrowserThread::IO); } scoped_refptr<base::SingleThreadTaskRunner> HeadlessBrowserImpl::BrowserMainThread() const { - return content::BrowserThread::GetMessageLoopProxyForThread( + return content::BrowserThread::GetTaskRunnerForThread( content::BrowserThread::UI); } -scoped_refptr<base::SingleThreadTaskRunner> -HeadlessBrowserImpl::BrowserFileThread() const { - return content::BrowserThread::GetMessageLoopProxyForThread( - content::BrowserThread::FILE); -} - void HeadlessBrowserImpl::Shutdown() { - DCHECK(BrowserMainThread()->BelongsToCurrentThread()); + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + weak_ptr_factory_.InvalidateWeakPtrs(); + + // Destroy all browser contexts. + browser_contexts_.clear(); + BrowserMainThread()->PostTask(FROM_HERE, base::MessageLoop::QuitWhenIdleClosure()); } -std::vector<HeadlessWebContents*> HeadlessBrowserImpl::GetAllWebContents() { - std::vector<HeadlessWebContents*> result; - result.reserve(web_contents_.size()); +std::vector<HeadlessBrowserContext*> +HeadlessBrowserImpl::GetAllBrowserContexts() { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + std::vector<HeadlessBrowserContext*> result; + result.reserve(browser_contexts_.size()); - for (const auto& web_contents_pair : web_contents_) { - result.push_back(web_contents_pair.first); + for (const auto& browser_context_pair : browser_contexts_) { + result.push_back(browser_context_pair.second.get()); } return result; } HeadlessBrowserMainParts* HeadlessBrowserImpl::browser_main_parts() const { - DCHECK(BrowserMainThread()->BelongsToCurrentThread()); + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); return browser_main_parts_; } @@ -127,10 +119,8 @@ void HeadlessBrowserImpl::set_browser_main_parts( void HeadlessBrowserImpl::RunOnStartCallback() { DCHECK(aura::Env::GetInstance()); - // TODO(eseckler): allow configuration of window (viewport) size by embedder. - const gfx::Size kDefaultSize(800, 600); window_tree_host_.reset( - aura::WindowTreeHost::Create(gfx::Rect(kDefaultSize))); + aura::WindowTreeHost::Create(gfx::Rect(options()->window_size))); window_tree_host_->InitHost(); window_tree_client_.reset( @@ -140,26 +130,57 @@ void HeadlessBrowserImpl::RunOnStartCallback() { on_start_callback_ = base::Callback<void(HeadlessBrowser*)>(); } -HeadlessWebContentsImpl* HeadlessBrowserImpl::RegisterWebContents( - std::unique_ptr<HeadlessWebContentsImpl> web_contents) { - DCHECK(web_contents); - HeadlessWebContentsImpl* unowned_web_contents = web_contents.get(); - web_contents_[unowned_web_contents] = std::move(web_contents); - return unowned_web_contents; +HeadlessBrowserContext* HeadlessBrowserImpl::CreateBrowserContext( + HeadlessBrowserContext::Builder* builder) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + std::unique_ptr<HeadlessBrowserContextImpl> browser_context = + HeadlessBrowserContextImpl::Create(builder); + + if (!browser_context) { + return nullptr; + } + + HeadlessBrowserContext* result = browser_context.get(); + + browser_contexts_[browser_context->Id()] = std::move(browser_context); + + return result; } -void HeadlessBrowserImpl::DestroyWebContents( - HeadlessWebContentsImpl* web_contents) { - auto it = web_contents_.find(web_contents); - DCHECK(it != web_contents_.end()); - web_contents_.erase(it); +void HeadlessBrowserImpl::DestroyBrowserContext( + HeadlessBrowserContextImpl* browser_context) { + auto it = browser_contexts_.find(browser_context->Id()); + DCHECK(it != browser_contexts_.end()); + browser_contexts_.erase(it); } -void HeadlessBrowserImpl::SetOptionsForTesting( - HeadlessBrowser::Options options) { - options_ = std::move(options); - browser_main_parts()->default_browser_context()->SetOptionsForTesting( - &options_); +base::WeakPtr<HeadlessBrowserImpl> HeadlessBrowserImpl::GetWeakPtr() { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + return weak_ptr_factory_.GetWeakPtr(); +} + +aura::WindowTreeHost* HeadlessBrowserImpl::window_tree_host() const { + return window_tree_host_.get(); +} + +HeadlessWebContents* HeadlessBrowserImpl::GetWebContentsForDevToolsAgentHostId( + const std::string& devtools_agent_host_id) { + for (HeadlessBrowserContext* context : GetAllBrowserContexts()) { + HeadlessWebContents* web_contents = + context->GetWebContentsForDevToolsAgentHostId(devtools_agent_host_id); + if (web_contents) + return web_contents; + } + return nullptr; +} + +HeadlessBrowserContext* HeadlessBrowserImpl::GetBrowserContextForId( + const std::string& id) { + auto find_it = browser_contexts_.find(id); + if (find_it == browser_contexts_.end()) + return nullptr; + return find_it->second.get(); } void RunChildProcessIfNeeded(int argc, const char** argv) { diff --git a/chromium/headless/lib/browser/headless_browser_impl.h b/chromium/headless/lib/browser/headless_browser_impl.h index 68dc676e5dd..a31391816c0 100644 --- a/chromium/headless/lib/browser/headless_browser_impl.h +++ b/chromium/headless/lib/browser/headless_browser_impl.h @@ -8,9 +8,12 @@ #include "headless/public/headless_browser.h" #include <memory> +#include <string> #include <unordered_map> #include <vector> +#include "base/memory/weak_ptr.h" +#include "headless/lib/browser/headless_devtools_manager_delegate.h" #include "headless/lib/browser/headless_web_contents_impl.h" namespace aura { @@ -23,7 +26,7 @@ class WindowTreeClient; namespace headless { -class HeadlessBrowserContext; +class HeadlessBrowserContextImpl; class HeadlessBrowserMainParts; class HeadlessBrowserImpl : public HeadlessBrowser { @@ -34,18 +37,20 @@ class HeadlessBrowserImpl : public HeadlessBrowser { ~HeadlessBrowserImpl() override; // HeadlessBrowser implementation: - HeadlessWebContents::Builder CreateWebContentsBuilder() override; HeadlessBrowserContext::Builder CreateBrowserContextBuilder() override; - HeadlessWebContents* CreateWebContents(const GURL& initial_url, - const gfx::Size& size) override; - scoped_refptr<base::SingleThreadTaskRunner> BrowserMainThread() - const override; scoped_refptr<base::SingleThreadTaskRunner> BrowserFileThread() const override; + scoped_refptr<base::SingleThreadTaskRunner> BrowserIOThread() const override; + scoped_refptr<base::SingleThreadTaskRunner> BrowserMainThread() + const override; void Shutdown() override; - std::vector<HeadlessWebContents*> GetAllWebContents() override; + std::vector<HeadlessBrowserContext*> GetAllBrowserContexts() override; + HeadlessWebContents* GetWebContentsForDevToolsAgentHostId( + const std::string& devtools_agent_host_id) override; + HeadlessBrowserContext* GetBrowserContextForId( + const std::string& id) override; void set_browser_main_parts(HeadlessBrowserMainParts* browser_main_parts); HeadlessBrowserMainParts* browser_main_parts() const; @@ -54,27 +59,31 @@ class HeadlessBrowserImpl : public HeadlessBrowser { HeadlessBrowser::Options* options() { return &options_; } - HeadlessWebContents* CreateWebContents(HeadlessWebContents::Builder* builder); - HeadlessWebContentsImpl* RegisterWebContents( - std::unique_ptr<HeadlessWebContentsImpl> web_contents); + HeadlessBrowserContext* CreateBrowserContext( + HeadlessBrowserContext::Builder* builder); + // Close given |browser_context| and delete it + // (all web contents associated with it go away too). + void DestroyBrowserContext(HeadlessBrowserContextImpl* browser_context); - // Close given |web_contents| and delete it. - void DestroyWebContents(HeadlessWebContentsImpl* web_contents); + base::WeakPtr<HeadlessBrowserImpl> GetWeakPtr(); - // Customize the options used by this headless browser instance. Note that - // options which take effect before the message loop has been started (e.g., - // custom message pumps) cannot be set via this method. - void SetOptionsForTesting(HeadlessBrowser::Options options); + aura::WindowTreeHost* window_tree_host() const; protected: base::Callback<void(HeadlessBrowser*)> on_start_callback_; HeadlessBrowser::Options options_; HeadlessBrowserMainParts* browser_main_parts_; // Not owned. + + // TODO(eseckler): Currently one window and one window_tree_host + // is used for all web contents. We should probably use one + // window per web contents, but additional investigation is needed. std::unique_ptr<aura::WindowTreeHost> window_tree_host_; std::unique_ptr<aura::client::WindowTreeClient> window_tree_client_; - std::unordered_map<HeadlessWebContents*, std::unique_ptr<HeadlessWebContents>> - web_contents_; + std::unordered_map<std::string, std::unique_ptr<HeadlessBrowserContextImpl>> + browser_contexts_; + + base::WeakPtrFactory<HeadlessBrowserImpl> weak_ptr_factory_; private: DISALLOW_COPY_AND_ASSIGN(HeadlessBrowserImpl); diff --git a/chromium/headless/lib/browser/headless_browser_main_parts.cc b/chromium/headless/lib/browser/headless_browser_main_parts.cc index c970b790739..b219480b6f7 100644 --- a/chromium/headless/lib/browser/headless_browser_main_parts.cc +++ b/chromium/headless/lib/browser/headless_browser_main_parts.cc @@ -4,7 +4,6 @@ #include "headless/lib/browser/headless_browser_main_parts.h" -#include "components/devtools_http_handler/devtools_http_handler.h" #include "headless/lib/browser/headless_browser_context_impl.h" #include "headless/lib/browser/headless_browser_impl.h" #include "headless/lib/browser/headless_devtools.h" @@ -16,8 +15,8 @@ namespace headless { namespace { -void PlatformInitialize() { - HeadlessScreen* screen = HeadlessScreen::Create(gfx::Size()); +void PlatformInitialize(const gfx::Size& screen_size) { + HeadlessScreen* screen = HeadlessScreen::Create(screen_size); display::Screen::SetScreenInstance(screen); } @@ -27,29 +26,25 @@ void PlatformExit() { } // namespace HeadlessBrowserMainParts::HeadlessBrowserMainParts(HeadlessBrowserImpl* browser) - : browser_(browser) {} + : browser_(browser) + , devtools_http_handler_started_(false) {} HeadlessBrowserMainParts::~HeadlessBrowserMainParts() {} void HeadlessBrowserMainParts::PreMainMessageLoopRun() { - browser_context_.reset(new HeadlessBrowserContextImpl(ProtocolHandlerMap(), - browser_->options())); if (browser_->options()->devtools_endpoint.address().IsValid()) { - devtools_http_handler_ = - CreateLocalDevToolsHttpHandler(browser_context_.get()); + StartLocalDevToolsHttpHandler(browser_->options()); + devtools_http_handler_started_ = true; } - PlatformInitialize(); + PlatformInitialize(browser_->options()->window_size); } void HeadlessBrowserMainParts::PostMainMessageLoopRun() { - browser_context_.reset(); - devtools_http_handler_.reset(); + if (devtools_http_handler_started_) { + StopLocalDevToolsHttpHandler(); + devtools_http_handler_started_ = false; + } PlatformExit(); } -HeadlessBrowserContextImpl* HeadlessBrowserMainParts::default_browser_context() - const { - return browser_context_.get(); -} - } // namespace headless diff --git a/chromium/headless/lib/browser/headless_browser_main_parts.h b/chromium/headless/lib/browser/headless_browser_main_parts.h index b0e30c397b8..97bc9e9e912 100644 --- a/chromium/headless/lib/browser/headless_browser_main_parts.h +++ b/chromium/headless/lib/browser/headless_browser_main_parts.h @@ -10,10 +10,6 @@ #include "content/public/browser/browser_main_parts.h" #include "headless/public/headless_browser.h" -namespace devtools_http_handler { -class DevToolsHttpHandler; -} - namespace headless { class HeadlessBrowserContextImpl; @@ -28,13 +24,10 @@ class HeadlessBrowserMainParts : public content::BrowserMainParts { void PreMainMessageLoopRun() override; void PostMainMessageLoopRun() override; - HeadlessBrowserContextImpl* default_browser_context() const; - private: HeadlessBrowserImpl* browser_; // Not owned. - std::unique_ptr<HeadlessBrowserContextImpl> browser_context_; - std::unique_ptr<devtools_http_handler::DevToolsHttpHandler> - devtools_http_handler_; + + bool devtools_http_handler_started_; DISALLOW_COPY_AND_ASSIGN(HeadlessBrowserMainParts); }; diff --git a/chromium/headless/lib/browser/headless_content_browser_client.cc b/chromium/headless/lib/browser/headless_content_browser_client.cc index a56a1fef4d9..4b59e918e5c 100644 --- a/chromium/headless/lib/browser/headless_content_browser_client.cc +++ b/chromium/headless/lib/browser/headless_content_browser_client.cc @@ -6,10 +6,16 @@ #include <memory> +#include "base/callback.h" #include "base/memory/ptr_util.h" +#include "content/public/browser/browser_context.h" #include "content/public/browser/browser_thread.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/render_view_host.h" +#include "headless/lib/browser/headless_browser_context_impl.h" #include "headless/lib/browser/headless_browser_impl.h" #include "headless/lib/browser/headless_browser_main_parts.h" +#include "headless/lib/browser/headless_devtools_manager_delegate.h" namespace headless { @@ -22,9 +28,25 @@ HeadlessContentBrowserClient::~HeadlessContentBrowserClient() {} content::BrowserMainParts* HeadlessContentBrowserClient::CreateBrowserMainParts( const content::MainFunctionParams&) { std::unique_ptr<HeadlessBrowserMainParts> browser_main_parts = - base::WrapUnique(new HeadlessBrowserMainParts(browser_)); + base::MakeUnique<HeadlessBrowserMainParts>(browser_); browser_->set_browser_main_parts(browser_main_parts.get()); return browser_main_parts.release(); } +void HeadlessContentBrowserClient::OverrideWebkitPrefs( + content::RenderViewHost* render_view_host, + content::WebPreferences* prefs) { + auto browser_context = HeadlessBrowserContextImpl::From( + render_view_host->GetProcess()->GetBrowserContext()); + const base::Callback<void(headless::WebPreferences*)>& callback = + browser_context->options()->override_web_preferences_callback(); + if (callback) + callback.Run(prefs); +} + +content::DevToolsManagerDelegate* +HeadlessContentBrowserClient::GetDevToolsManagerDelegate() { + return new HeadlessDevToolsManagerDelegate(browser_->GetWeakPtr()); +} + } // namespace headless diff --git a/chromium/headless/lib/browser/headless_content_browser_client.h b/chromium/headless/lib/browser/headless_content_browser_client.h index 16d9ef300ae..0bd58b12246 100644 --- a/chromium/headless/lib/browser/headless_content_browser_client.h +++ b/chromium/headless/lib/browser/headless_content_browser_client.h @@ -11,6 +11,7 @@ namespace headless { class HeadlessBrowserImpl; class HeadlessBrowserMainParts; +class HeadlessDevToolsManagerDelegate; class HeadlessContentBrowserClient : public content::ContentBrowserClient { public: @@ -20,6 +21,9 @@ class HeadlessContentBrowserClient : public content::ContentBrowserClient { // content::ContentBrowserClient implementation: content::BrowserMainParts* CreateBrowserMainParts( const content::MainFunctionParams&) override; + void OverrideWebkitPrefs(content::RenderViewHost* render_view_host, + content::WebPreferences* prefs) override; + content::DevToolsManagerDelegate* GetDevToolsManagerDelegate() override; private: HeadlessBrowserImpl* browser_; // Not owned. diff --git a/chromium/headless/lib/browser/headless_devtools.cc b/chromium/headless/lib/browser/headless_devtools.cc index 626a29de89e..ea97afcc96a 100644 --- a/chromium/headless/lib/browser/headless_devtools.cc +++ b/chromium/headless/lib/browser/headless_devtools.cc @@ -4,101 +4,71 @@ #include "headless/lib/browser/headless_devtools.h" +#include <string> +#include <utility> + #include "base/files/file_path.h" #include "base/memory/ptr_util.h" -#include "components/devtools_http_handler/devtools_http_handler.h" -#include "components/devtools_http_handler/devtools_http_handler_delegate.h" +#include "content/public/browser/devtools_agent_host.h" #include "content/public/browser/devtools_frontend_host.h" +#include "content/public/browser/devtools_socket_factory.h" #include "content/public/browser/navigation_entry.h" #include "headless/grit/headless_lib_resources.h" -#include "headless/lib/browser/headless_browser_context_impl.h" +#include "headless/public/headless_browser.h" #include "net/base/net_errors.h" +#include "net/log/net_log_source.h" #include "net/socket/tcp_server_socket.h" #include "ui/base/resource/resource_bundle.h" -using devtools_http_handler::DevToolsHttpHandler; - namespace headless { namespace { const int kBackLog = 10; -class TCPServerSocketFactory : public DevToolsHttpHandler::ServerSocketFactory { +class TCPServerSocketFactory : public content::DevToolsSocketFactory { public: - TCPServerSocketFactory(const net::IPEndPoint& endpoint) + explicit TCPServerSocketFactory(const net::IPEndPoint& endpoint) : endpoint_(endpoint) { DCHECK(endpoint_.address().IsValid()); } private: - // DevToolsHttpHandler::ServerSocketFactory implementation: + // content::DevToolsSocketFactory implementation: std::unique_ptr<net::ServerSocket> CreateForHttpServer() override { std::unique_ptr<net::ServerSocket> socket( - new net::TCPServerSocket(nullptr, net::NetLog::Source())); + new net::TCPServerSocket(nullptr, net::NetLogSource())); if (socket->Listen(endpoint_, kBackLog) != net::OK) return std::unique_ptr<net::ServerSocket>(); return socket; } + std::unique_ptr<net::ServerSocket> CreateForTethering( + std::string* out_name) override { + return nullptr; + } + net::IPEndPoint endpoint_; DISALLOW_COPY_AND_ASSIGN(TCPServerSocketFactory); }; -class HeadlessDevToolsDelegate - : public devtools_http_handler::DevToolsHttpHandlerDelegate { - public: - HeadlessDevToolsDelegate(); - ~HeadlessDevToolsDelegate() override; - - // devtools_http_handler::DevToolsHttpHandlerDelegate implementation: - std::string GetDiscoveryPageHTML() override; - std::string GetFrontendResource(const std::string& path) override; - std::string GetPageThumbnailData(const GURL& url) override; - content::DevToolsExternalAgentProxyDelegate* HandleWebSocketConnection( - const std::string& path) override; - - private: - DISALLOW_COPY_AND_ASSIGN(HeadlessDevToolsDelegate); -}; - -HeadlessDevToolsDelegate::HeadlessDevToolsDelegate() {} - -HeadlessDevToolsDelegate::~HeadlessDevToolsDelegate() {} - -std::string HeadlessDevToolsDelegate::GetDiscoveryPageHTML() { - return ResourceBundle::GetSharedInstance().GetRawDataResource( - IDR_HEADLESS_LIB_DEVTOOLS_DISCOVERY_PAGE).as_string(); -} - -std::string HeadlessDevToolsDelegate::GetFrontendResource( - const std::string& path) { - return content::DevToolsFrontendHost::GetFrontendResource(path).as_string(); -} - -std::string HeadlessDevToolsDelegate::GetPageThumbnailData(const GURL& url) { - return std::string(); -} - -content::DevToolsExternalAgentProxyDelegate* -HeadlessDevToolsDelegate::HandleWebSocketConnection(const std::string& path) { - return nullptr; -} - } // namespace -std::unique_ptr<DevToolsHttpHandler> CreateLocalDevToolsHttpHandler( - HeadlessBrowserContextImpl* browser_context) { - const net::IPEndPoint& endpoint = - browser_context->options()->devtools_endpoint; - std::unique_ptr<DevToolsHttpHandler::ServerSocketFactory> socket_factory( +void StartLocalDevToolsHttpHandler( + HeadlessBrowser::Options* options) { + const net::IPEndPoint& endpoint = options->devtools_endpoint; + std::unique_ptr<content::DevToolsSocketFactory> socket_factory( new TCPServerSocketFactory(endpoint)); - return base::WrapUnique(new DevToolsHttpHandler( - std::move(socket_factory), std::string(), new HeadlessDevToolsDelegate(), - browser_context->GetPath(), base::FilePath(), std::string(), - browser_context->options()->user_agent)); + content::DevToolsAgentHost::StartRemoteDebuggingServer( + std::move(socket_factory), std::string(), + options->user_data_dir, // TODO(altimin): Figure a proper value for this. + base::FilePath(), std::string(), options->user_agent); +} + +void StopLocalDevToolsHttpHandler() { + content::DevToolsAgentHost::StopRemoteDebuggingServer(); } } // namespace headless diff --git a/chromium/headless/lib/browser/headless_devtools.h b/chromium/headless/lib/browser/headless_devtools.h index b2b4a6aff80..eccdd088806 100644 --- a/chromium/headless/lib/browser/headless_devtools.h +++ b/chromium/headless/lib/browser/headless_devtools.h @@ -7,18 +7,15 @@ #include <memory> -namespace devtools_http_handler { -class DevToolsHttpHandler; -} +#include "headless/public/headless_browser.h" namespace headless { -class HeadlessBrowserContextImpl; // Starts a DevTools HTTP handler on the loopback interface on the port // configured by HeadlessBrowser::Options. -std::unique_ptr<devtools_http_handler::DevToolsHttpHandler> -CreateLocalDevToolsHttpHandler(HeadlessBrowserContextImpl* browser_context); +void StartLocalDevToolsHttpHandler(HeadlessBrowser::Options* options); +void StopLocalDevToolsHttpHandler(); -} // namespace content +} // namespace headless #endif // HEADLESS_LIB_BROWSER_HEADLESS_DEVTOOLS_H_ diff --git a/chromium/headless/lib/browser/headless_devtools_client_impl.cc b/chromium/headless/lib/browser/headless_devtools_client_impl.cc index 41aa4fc6994..a669e1e5b1d 100644 --- a/chromium/headless/lib/browser/headless_devtools_client_impl.cc +++ b/chromium/headless/lib/browser/headless_devtools_client_impl.cc @@ -6,10 +6,12 @@ #include <memory> +#include "base/bind.h" #include "base/json/json_reader.h" #include "base/json/json_writer.h" #include "base/memory/ptr_util.h" #include "base/values.h" +#include "content/public/browser/browser_thread.h" #include "content/public/browser/devtools_agent_host.h" namespace headless { @@ -33,6 +35,7 @@ HeadlessDevToolsClientImpl::HeadlessDevToolsClientImpl() accessibility_domain_(this), animation_domain_(this), application_cache_domain_(this), + browser_domain_(this), cache_storage_domain_(this), console_domain_(this), css_domain_(this), @@ -49,6 +52,7 @@ HeadlessDevToolsClientImpl::HeadlessDevToolsClientImpl() inspector_domain_(this), io_domain_(this), layer_tree_domain_(this), + log_domain_(this), memory_domain_(this), network_domain_(this), page_domain_(this), @@ -58,7 +62,10 @@ HeadlessDevToolsClientImpl::HeadlessDevToolsClientImpl() security_domain_(this), service_worker_domain_(this), tracing_domain_(this), - worker_domain_(this) {} + worker_domain_(this), + browser_main_thread_(content::BrowserThread::GetTaskRunnerForThread( + content::BrowserThread::UI)), + weak_ptr_factory_(this) {} HeadlessDevToolsClientImpl::~HeadlessDevToolsClientImpl() {} @@ -88,8 +95,10 @@ void HeadlessDevToolsClientImpl::DispatchProtocolMessage( NOTREACHED() << "Badly formed reply"; return; } - if (!DispatchMessageReply(*message_dict) && !DispatchEvent(*message_dict)) + if (!DispatchMessageReply(*message_dict) && + !DispatchEvent(std::move(message), *message_dict)) { DLOG(ERROR) << "Unhandled protocol message: " << json_message; + } } bool HeadlessDevToolsClientImpl::DispatchMessageReply( @@ -118,11 +127,12 @@ bool HeadlessDevToolsClientImpl::DispatchMessageReply( } bool HeadlessDevToolsClientImpl::DispatchEvent( + std::unique_ptr<base::Value> owning_message, const base::DictionaryValue& message_dict) { std::string method; if (!message_dict.GetString("method", &method)) return false; - auto it = event_handlers_.find(method); + EventHandlerMap::const_iterator it = event_handlers_.find(method); if (it == event_handlers_.end()) { NOTREACHED() << "Unknown event: " << method; return false; @@ -133,11 +143,24 @@ bool HeadlessDevToolsClientImpl::DispatchEvent( NOTREACHED() << "Badly formed event parameters"; return false; } - it->second.Run(*result_dict); + // DevTools assumes event handling is async so we must post a task here or + // we risk breaking things. + browser_main_thread_->PostTask( + FROM_HERE, base::Bind(&HeadlessDevToolsClientImpl::DispatchEventTask, + weak_ptr_factory_.GetWeakPtr(), + base::Passed(std::move(owning_message)), + &it->second, result_dict)); } return true; } +void HeadlessDevToolsClientImpl::DispatchEventTask( + std::unique_ptr<base::Value> owning_message, + const EventHandler* event_handler, + const base::DictionaryValue* result_dict) { + event_handler->Run(*result_dict); +} + void HeadlessDevToolsClientImpl::AgentHostClosed( content::DevToolsAgentHost* agent_host, bool replaced_with_another_client) { @@ -158,6 +181,10 @@ application_cache::Domain* HeadlessDevToolsClientImpl::GetApplicationCache() { return &application_cache_domain_; } +browser::Domain* HeadlessDevToolsClientImpl::GetBrowser() { + return &browser_domain_; +} + cache_storage::Domain* HeadlessDevToolsClientImpl::GetCacheStorage() { return &cache_storage_domain_; } @@ -222,6 +249,10 @@ layer_tree::Domain* HeadlessDevToolsClientImpl::GetLayerTree() { return &layer_tree_domain_; } +log::Domain* HeadlessDevToolsClientImpl::GetLog() { + return &log_domain_; +} + memory::Domain* HeadlessDevToolsClientImpl::GetMemory() { return &memory_domain_; } diff --git a/chromium/headless/lib/browser/headless_devtools_client_impl.h b/chromium/headless/lib/browser/headless_devtools_client_impl.h index c7dbb52db45..619825d28ba 100644 --- a/chromium/headless/lib/browser/headless_devtools_client_impl.h +++ b/chromium/headless/lib/browser/headless_devtools_client_impl.h @@ -7,10 +7,13 @@ #include <unordered_map> +#include "base/memory/weak_ptr.h" +#include "base/single_thread_task_runner.h" #include "content/public/browser/devtools_agent_host_client.h" #include "headless/public/domains/accessibility.h" #include "headless/public/domains/animation.h" #include "headless/public/domains/application_cache.h" +#include "headless/public/domains/browser.h" #include "headless/public/domains/cache_storage.h" #include "headless/public/domains/console.h" #include "headless/public/domains/css.h" @@ -27,6 +30,7 @@ #include "headless/public/domains/inspector.h" #include "headless/public/domains/io.h" #include "headless/public/domains/layer_tree.h" +#include "headless/public/domains/log.h" #include "headless/public/domains/memory.h" #include "headless/public/domains/network.h" #include "headless/public/domains/page.h" @@ -63,6 +67,7 @@ class HeadlessDevToolsClientImpl : public HeadlessDevToolsClient, accessibility::Domain* GetAccessibility() override; animation::Domain* GetAnimation() override; application_cache::Domain* GetApplicationCache() override; + browser::Domain* GetBrowser() override; cache_storage::Domain* GetCacheStorage() override; console::Domain* GetConsole() override; css::Domain* GetCSS() override; @@ -79,6 +84,7 @@ class HeadlessDevToolsClientImpl : public HeadlessDevToolsClient, inspector::Domain* GetInspector() override; io::Domain* GetIO() override; layer_tree::Domain* GetLayerTree() override; + log::Domain* GetLog() override; memory::Domain* GetMemory() override; network::Domain* GetNetwork() override; page::Domain* GetPage() override; @@ -143,17 +149,26 @@ class HeadlessDevToolsClientImpl : public HeadlessDevToolsClient, void SendMessageWithoutParams(const char* method, CallbackType callback); bool DispatchMessageReply(const base::DictionaryValue& message_dict); - bool DispatchEvent(const base::DictionaryValue& message_dict); + + using EventHandler = base::Callback<void(const base::Value&)>; + using EventHandlerMap = std::unordered_map<std::string, EventHandler>; + + bool DispatchEvent(std::unique_ptr<base::Value> owning_message, + const base::DictionaryValue& message_dict); + void DispatchEventTask(std::unique_ptr<base::Value> owning_message, + const EventHandler* event_handler, + const base::DictionaryValue* result_dict); content::DevToolsAgentHost* agent_host_; // Not owned. int next_message_id_; std::unordered_map<int, Callback> pending_messages_; - std::unordered_map<std::string, base::Callback<void(const base::Value&)>> - event_handlers_; + + EventHandlerMap event_handlers_; accessibility::ExperimentalDomain accessibility_domain_; animation::ExperimentalDomain animation_domain_; application_cache::ExperimentalDomain application_cache_domain_; + browser::ExperimentalDomain browser_domain_; cache_storage::ExperimentalDomain cache_storage_domain_; console::ExperimentalDomain console_domain_; css::ExperimentalDomain css_domain_; @@ -170,6 +185,7 @@ class HeadlessDevToolsClientImpl : public HeadlessDevToolsClient, inspector::ExperimentalDomain inspector_domain_; io::ExperimentalDomain io_domain_; layer_tree::ExperimentalDomain layer_tree_domain_; + log::ExperimentalDomain log_domain_; memory::ExperimentalDomain memory_domain_; network::ExperimentalDomain network_domain_; page::ExperimentalDomain page_domain_; @@ -180,6 +196,8 @@ class HeadlessDevToolsClientImpl : public HeadlessDevToolsClient, service_worker::ExperimentalDomain service_worker_domain_; tracing::ExperimentalDomain tracing_domain_; worker::ExperimentalDomain worker_domain_; + scoped_refptr<base::SingleThreadTaskRunner> browser_main_thread_; + base::WeakPtrFactory<HeadlessDevToolsClientImpl> weak_ptr_factory_; DISALLOW_COPY_AND_ASSIGN(HeadlessDevToolsClientImpl); }; diff --git a/chromium/headless/lib/browser/headless_devtools_manager_delegate.cc b/chromium/headless/lib/browser/headless_devtools_manager_delegate.cc new file mode 100644 index 00000000000..a53e8c773df --- /dev/null +++ b/chromium/headless/lib/browser/headless_devtools_manager_delegate.cc @@ -0,0 +1,167 @@ +// 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 "headless/lib/browser/headless_devtools_manager_delegate.h" + +#include <string> +#include <utility> + +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/devtools_agent_host.h" +#include "content/public/browser/devtools_frontend_host.h" +#include "content/public/browser/web_contents.h" +#include "headless/grit/headless_lib_resources.h" +#include "headless/lib/browser/headless_browser_context_impl.h" +#include "headless/lib/browser/headless_browser_impl.h" +#include "headless/lib/browser/headless_web_contents_impl.h" +#include "headless/public/domains/browser.h" +#include "ui/base/resource/resource_bundle.h" + +namespace headless { + +HeadlessDevToolsManagerDelegate::HeadlessDevToolsManagerDelegate( + base::WeakPtr<HeadlessBrowserImpl> browser) + : browser_(std::move(browser)), default_browser_context_(nullptr) { + command_map_["Browser.createTarget"] = + &HeadlessDevToolsManagerDelegate::CreateTarget; + command_map_["Browser.closeTarget"] = + &HeadlessDevToolsManagerDelegate::CloseTarget; + command_map_["Browser.createBrowserContext"] = + &HeadlessDevToolsManagerDelegate::CreateBrowserContext; + command_map_["Browser.disposeBrowserContext"] = + &HeadlessDevToolsManagerDelegate::DisposeBrowserContext; +} + +HeadlessDevToolsManagerDelegate::~HeadlessDevToolsManagerDelegate() {} + +base::DictionaryValue* HeadlessDevToolsManagerDelegate::HandleCommand( + content::DevToolsAgentHost* agent_host, + base::DictionaryValue* command) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + if (!browser_) + return nullptr; + + int id; + std::string method; + const base::DictionaryValue* params = nullptr; + if (!command->GetInteger("id", &id) || + !command->GetString("method", &method) || + !command->GetDictionary("params", ¶ms)) { + return nullptr; + } + auto find_it = command_map_.find(method); + if (find_it == command_map_.end()) + return nullptr; + CommandMemberFnPtr command_fn_ptr = find_it->second; + std::unique_ptr<base::Value> cmd_result(((this)->*command_fn_ptr)(params)); + if (!cmd_result) + return nullptr; + + std::unique_ptr<base::DictionaryValue> result(new base::DictionaryValue()); + result->SetInteger("id", id); + result->Set("result", std::move(cmd_result)); + return result.release(); +} + +std::string HeadlessDevToolsManagerDelegate::GetDiscoveryPageHTML() { + return ResourceBundle::GetSharedInstance().GetRawDataResource( + IDR_HEADLESS_LIB_DEVTOOLS_DISCOVERY_PAGE).as_string(); +} + +std::string HeadlessDevToolsManagerDelegate::GetFrontendResource( + const std::string& path) { + return content::DevToolsFrontendHost::GetFrontendResource(path).as_string(); +} + +std::unique_ptr<base::Value> HeadlessDevToolsManagerDelegate::CreateTarget( + const base::DictionaryValue* params) { + std::string url; + std::string browser_context_id; + int width = browser_->options()->window_size.width(); + int height = browser_->options()->window_size.height(); + params->GetString("url", &url); + params->GetString("browserContextId", &browser_context_id); + params->GetInteger("width", &width); + params->GetInteger("height", &height); + + // TODO(alexclarke): Should we fail when user passes incorrect id? + HeadlessBrowserContext* context = + browser_->GetBrowserContextForId(browser_context_id); + if (!context) { + if (!default_browser_context_) { + default_browser_context_ = + browser_->CreateBrowserContextBuilder().Build(); + } + context = default_browser_context_; + } + + HeadlessWebContentsImpl* web_contents_impl = + HeadlessWebContentsImpl::From(context->CreateWebContentsBuilder() + .SetInitialURL(GURL(url)) + .SetWindowSize(gfx::Size(width, height)) + .Build()); + + return browser::CreateTargetResult::Builder() + .SetTargetId(web_contents_impl->GetDevToolsAgentHostId()) + .Build() + ->Serialize(); +} + +std::unique_ptr<base::Value> HeadlessDevToolsManagerDelegate::CloseTarget( + const base::DictionaryValue* params) { + std::string target_id; + if (!params->GetString("targetId", &target_id)) { + return nullptr; + } + HeadlessWebContents* web_contents = + browser_->GetWebContentsForDevToolsAgentHostId(target_id); + bool success = false; + if (web_contents) { + web_contents->Close(); + success = true; + } + return browser::CloseTargetResult::Builder() + .SetSuccess(success) + .Build() + ->Serialize(); +} + +std::unique_ptr<base::Value> +HeadlessDevToolsManagerDelegate::CreateBrowserContext( + const base::DictionaryValue* params) { + HeadlessBrowserContext* browser_context = + browser_->CreateBrowserContextBuilder().Build(); + + std::unique_ptr<base::DictionaryValue> result(new base::DictionaryValue()); + return browser::CreateBrowserContextResult::Builder() + .SetBrowserContextId(browser_context->Id()) + .Build() + ->Serialize(); +} + +std::unique_ptr<base::Value> +HeadlessDevToolsManagerDelegate::DisposeBrowserContext( + const base::DictionaryValue* params) { + std::string browser_context_id; + if (!params->GetString("browserContextId", &browser_context_id)) { + return nullptr; + } + HeadlessBrowserContext* context = + browser_->GetBrowserContextForId(browser_context_id); + + bool success = false; + if (context && context != default_browser_context_ && + context->GetAllWebContents().empty()) { + success = true; + context->Close(); + } + + return browser::DisposeBrowserContextResult::Builder() + .SetSuccess(success) + .Build() + ->Serialize(); +} + +} // namespace headless diff --git a/chromium/headless/lib/browser/headless_devtools_manager_delegate.h b/chromium/headless/lib/browser/headless_devtools_manager_delegate.h new file mode 100644 index 00000000000..4d43fc76b79 --- /dev/null +++ b/chromium/headless/lib/browser/headless_devtools_manager_delegate.h @@ -0,0 +1,56 @@ +// 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 HEADLESS_LIB_BROWSER_HEADLESS_DEVTOOLS_MANAGER_DELEGATE_H_ +#define HEADLESS_LIB_BROWSER_HEADLESS_DEVTOOLS_MANAGER_DELEGATE_H_ + +#include "content/public/browser/devtools_manager_delegate.h" + +#include <map> +#include <memory> +#include <string> + +#include "base/memory/weak_ptr.h" +#include "base/values.h" + +namespace headless { +class HeadlessBrowserImpl; +class HeadlessBrowserContext; +class HeadlessWebContentsImpl; + +class HeadlessDevToolsManagerDelegate + : public content::DevToolsManagerDelegate { + public: + explicit HeadlessDevToolsManagerDelegate( + base::WeakPtr<HeadlessBrowserImpl> browser); + ~HeadlessDevToolsManagerDelegate() override; + + // DevToolsManagerDelegate implementation: + base::DictionaryValue* HandleCommand(content::DevToolsAgentHost* agent_host, + base::DictionaryValue* command) override; + std::string GetDiscoveryPageHTML() override; + std::string GetFrontendResource(const std::string& path) override; + + private: + std::unique_ptr<base::Value> CreateTarget( + const base::DictionaryValue* params); + std::unique_ptr<base::Value> CloseTarget(const base::DictionaryValue* params); + std::unique_ptr<base::Value> CreateBrowserContext( + const base::DictionaryValue* params); + std::unique_ptr<base::Value> DisposeBrowserContext( + const base::DictionaryValue* params); + + base::WeakPtr<HeadlessBrowserImpl> browser_; + + using CommandMemberFnPtr = std::unique_ptr<base::Value> ( + HeadlessDevToolsManagerDelegate::*)(const base::DictionaryValue* params); + + std::map<std::string, CommandMemberFnPtr> command_map_; + + HeadlessBrowserContext* default_browser_context_; +}; + +} // namespace headless + +#endif // HEADLESS_LIB_BROWSER_HEADLESS_DEVTOOLS_MANAGER_DELEGATE_H_ diff --git a/chromium/headless/lib/browser/headless_screen.cc b/chromium/headless/lib/browser/headless_screen.cc index e7be71a515b..c8be18bfdb1 100644 --- a/chromium/headless/lib/browser/headless_screen.cc +++ b/chromium/headless/lib/browser/headless_screen.cc @@ -30,8 +30,7 @@ bool IsRotationPortrait(display::Display::Rotation rotation) { // static HeadlessScreen* HeadlessScreen::Create(const gfx::Size& size) { - const gfx::Size kDefaultSize(800, 600); - return new HeadlessScreen(gfx::Rect(size.IsEmpty() ? kDefaultSize : size)); + return new HeadlessScreen(gfx::Rect(size)); } HeadlessScreen::~HeadlessScreen() {} diff --git a/chromium/headless/lib/browser/headless_screen.h b/chromium/headless/lib/browser/headless_screen.h index f0c363c54f6..962fc9c61b8 100644 --- a/chromium/headless/lib/browser/headless_screen.h +++ b/chromium/headless/lib/browser/headless_screen.h @@ -27,8 +27,7 @@ namespace headless { class HeadlessScreen : public display::Screen, public aura::WindowObserver { public: - // Creates a display::Screen of the specified size. If no size is specified, - // then creates a 800x600 screen. |size| is in physical pixels. + // Creates a display::Screen of the specified size (physical pixels). static HeadlessScreen* Create(const gfx::Size& size); ~HeadlessScreen() override; diff --git a/chromium/headless/lib/browser/headless_url_request_context_getter.cc b/chromium/headless/lib/browser/headless_url_request_context_getter.cc index d253e5c267f..04a04d5c25a 100644 --- a/chromium/headless/lib/browser/headless_url_request_context_getter.cc +++ b/chromium/headless/lib/browser/headless_url_request_context_getter.cc @@ -5,10 +5,13 @@ #include "headless/lib/browser/headless_url_request_context_getter.h" #include <memory> +#include <utility> +#include <vector> #include "base/memory/ptr_util.h" #include "base/single_thread_task_runner.h" #include "content/public/browser/browser_thread.h" +#include "headless/lib/browser/headless_browser_context_options.h" #include "net/dns/mapped_host_resolver.h" #include "net/proxy/proxy_service.h" #include "net/url_request/url_request_context.h" @@ -22,23 +25,18 @@ HeadlessURLRequestContextGetter::HeadlessURLRequestContextGetter( content::ProtocolHandlerMap* protocol_handlers, ProtocolHandlerMap context_protocol_handlers, content::URLRequestInterceptorScopedVector request_interceptors, - HeadlessBrowser::Options* options) + HeadlessBrowserContextOptions* options) : io_task_runner_(std::move(io_task_runner)), file_task_runner_(std::move(file_task_runner)), - user_agent_(options->user_agent), - host_resolver_rules_(options->host_resolver_rules), - proxy_server_(options->proxy_server), + user_agent_(options->user_agent()), + host_resolver_rules_(options->host_resolver_rules()), + proxy_server_(options->proxy_server()), request_interceptors_(std::move(request_interceptors)) { // Must first be created on the UI thread. DCHECK_CURRENTLY_ON(content::BrowserThread::UI); std::swap(protocol_handlers_, *protocol_handlers); - for (auto& pair : options->protocol_handlers) { - protocol_handlers_[pair.first] = - linked_ptr<net::URLRequestJobFactory::ProtocolHandler>( - pair.second.release()); - } - options->protocol_handlers.clear(); + for (auto& pair : context_protocol_handlers) { protocol_handlers_[pair.first] = linked_ptr<net::URLRequestJobFactory::ProtocolHandler>( @@ -67,9 +65,8 @@ HeadlessURLRequestContextGetter::GetURLRequestContext() { // TODO(skyostil): Make these configurable. builder.set_data_enabled(true); builder.set_file_enabled(true); - builder.SetFileTaskRunner( - content::BrowserThread::GetMessageLoopProxyForThread( - content::BrowserThread::FILE)); + builder.SetFileTaskRunner(content::BrowserThread::GetTaskRunnerForThread( + content::BrowserThread::FILE)); if (!proxy_server_.IsEmpty()) { builder.set_proxy_service( net::ProxyService::CreateFixed(proxy_server_.ToString())); @@ -108,7 +105,7 @@ HeadlessURLRequestContextGetter::GetURLRequestContext() { scoped_refptr<base::SingleThreadTaskRunner> HeadlessURLRequestContextGetter::GetNetworkTaskRunner() const { - return content::BrowserThread::GetMessageLoopProxyForThread( + return content::BrowserThread::GetTaskRunnerForThread( content::BrowserThread::IO); } diff --git a/chromium/headless/lib/browser/headless_url_request_context_getter.h b/chromium/headless/lib/browser/headless_url_request_context_getter.h index 5556ef19126..166f6a9cbe6 100644 --- a/chromium/headless/lib/browser/headless_url_request_context_getter.h +++ b/chromium/headless/lib/browser/headless_url_request_context_getter.h @@ -6,6 +6,7 @@ #define HEADLESS_LIB_BROWSER_HEADLESS_URL_REQUEST_CONTEXT_GETTER_H_ #include <memory> +#include <string> #include "base/compiler_specific.h" #include "base/files/file_path.h" @@ -33,6 +34,7 @@ class URLRequestContextStorage; } namespace headless { +class HeadlessBrowserContextOptions; class HeadlessURLRequestContextGetter : public net::URLRequestContextGetter { public: @@ -42,7 +44,7 @@ class HeadlessURLRequestContextGetter : public net::URLRequestContextGetter { content::ProtocolHandlerMap* protocol_handlers, ProtocolHandlerMap context_protocol_handlers, content::URLRequestInterceptorScopedVector request_interceptors, - HeadlessBrowser::Options* options); + HeadlessBrowserContextOptions* options); // net::URLRequestContextGetter implementation: net::URLRequestContext* GetURLRequestContext() override; diff --git a/chromium/headless/lib/browser/headless_web_contents_impl.cc b/chromium/headless/lib/browser/headless_web_contents_impl.cc index b07cb2c7ede..20865a3da63 100644 --- a/chromium/headless/lib/browser/headless_web_contents_impl.cc +++ b/chromium/headless/lib/browser/headless_web_contents_impl.cc @@ -4,10 +4,16 @@ #include "headless/lib/browser/headless_web_contents_impl.h" +#include <string> +#include <utility> + #include "base/bind.h" +#include "base/json/json_writer.h" #include "base/memory/ptr_util.h" #include "base/memory/weak_ptr.h" +#include "base/strings/utf_string_conversions.h" #include "base/trace_event/trace_event.h" +#include "content/public/browser/browser_thread.h" #include "content/public/browser/devtools_agent_host.h" #include "content/public/browser/navigation_handle.h" #include "content/public/browser/render_frame_host.h" @@ -16,17 +22,25 @@ #include "content/public/browser/render_widget_host_view.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_delegate.h" -#include "content/public/browser/web_contents_observer.h" #include "content/public/common/bindings_policy.h" #include "content/public/renderer/render_frame.h" #include "headless/lib/browser/headless_browser_context_impl.h" #include "headless/lib/browser/headless_browser_impl.h" #include "headless/lib/browser/headless_browser_main_parts.h" #include "headless/lib/browser/headless_devtools_client_impl.h" +#include "services/shell/public/cpp/interface_registry.h" #include "ui/aura/window.h" namespace headless { +// static +HeadlessWebContentsImpl* HeadlessWebContentsImpl::From( + HeadlessWebContents* web_contents) { + // This downcast is safe because there is only one implementation of + // HeadlessWebContents. + return static_cast<HeadlessWebContentsImpl*>(web_contents); +} + class WebContentsObserverAdapter : public content::WebContentsObserver { public: WebContentsObserverAdapter(content::WebContents* web_contents, @@ -48,36 +62,43 @@ class WebContentsObserverAdapter : public content::WebContentsObserver { class HeadlessWebContentsImpl::Delegate : public content::WebContentsDelegate { public: - explicit Delegate(HeadlessBrowserImpl* browser) : browser_(browser) {} + explicit Delegate(HeadlessBrowserContextImpl* browser_context) + : browser_context_(browser_context) {} void WebContentsCreated(content::WebContents* source_contents, + int opener_render_process_id, int opener_render_frame_id, const std::string& frame_name, const GURL& target_url, content::WebContents* new_contents) override { - browser_->RegisterWebContents( - HeadlessWebContentsImpl::CreateFromWebContents(new_contents, browser_)); + std::unique_ptr<HeadlessWebContentsImpl> web_contents = + HeadlessWebContentsImpl::CreateFromWebContents(new_contents, + browser_context_); + + DCHECK(new_contents->GetBrowserContext() == browser_context_); + + browser_context_->RegisterWebContents(std::move(web_contents)); } private: - HeadlessBrowserImpl* browser_; // Not owned. + HeadlessBrowserContextImpl* browser_context_; // Not owned. DISALLOW_COPY_AND_ASSIGN(Delegate); }; // static std::unique_ptr<HeadlessWebContentsImpl> HeadlessWebContentsImpl::Create( HeadlessWebContents::Builder* builder, - aura::Window* parent_window, - HeadlessBrowserImpl* browser) { - content::BrowserContext* context = - HeadlessBrowserContextImpl::From(builder->browser_context_); - content::WebContents::CreateParams create_params(context, nullptr); + aura::Window* parent_window) { + content::WebContents::CreateParams create_params(builder->browser_context_, + nullptr); create_params.initial_size = builder->window_size_; std::unique_ptr<HeadlessWebContentsImpl> headless_web_contents = base::WrapUnique(new HeadlessWebContentsImpl( - content::WebContents::Create(create_params), browser)); + content::WebContents::Create(create_params), + builder->browser_context_)); + headless_web_contents->mojo_services_ = std::move(builder->mojo_services_); headless_web_contents->InitializeScreen(parent_window, builder->window_size_); if (!headless_web_contents->OpenURL(builder->initial_url_)) return nullptr; @@ -88,9 +109,10 @@ std::unique_ptr<HeadlessWebContentsImpl> HeadlessWebContentsImpl::Create( std::unique_ptr<HeadlessWebContentsImpl> HeadlessWebContentsImpl::CreateFromWebContents( content::WebContents* web_contents, - HeadlessBrowserImpl* browser) { + HeadlessBrowserContextImpl* browser_context) { std::unique_ptr<HeadlessWebContentsImpl> headless_web_contents = - base::WrapUnique(new HeadlessWebContentsImpl(web_contents, browser)); + base::WrapUnique( + new HeadlessWebContentsImpl(web_contents, browser_context)); return headless_web_contents; } @@ -111,10 +133,13 @@ void HeadlessWebContentsImpl::InitializeScreen(aura::Window* parent_window, HeadlessWebContentsImpl::HeadlessWebContentsImpl( content::WebContents* web_contents, - HeadlessBrowserImpl* browser) - : web_contents_delegate_(new HeadlessWebContentsImpl::Delegate(browser)), + HeadlessBrowserContextImpl* browser_context) + : content::WebContentsObserver(web_contents), + web_contents_delegate_( + new HeadlessWebContentsImpl::Delegate(browser_context)), web_contents_(web_contents), - browser_(browser) { + agent_host_(content::DevToolsAgentHost::GetOrCreateFor(web_contents)), + browser_context_(browser_context) { web_contents_->SetDelegate(web_contents_delegate_.get()); } @@ -122,6 +147,23 @@ HeadlessWebContentsImpl::~HeadlessWebContentsImpl() { web_contents_->Close(); } +void HeadlessWebContentsImpl::RenderFrameCreated( + content::RenderFrameHost* render_frame_host) { + if (!mojo_services_.empty()) { + render_frame_host->GetRenderViewHost()->AllowBindings( + content::BINDINGS_POLICY_HEADLESS); + } + + shell::InterfaceRegistry* interface_registry = + render_frame_host->GetInterfaceRegistry(); + + for (const MojoService& service : mojo_services_) { + interface_registry->AddInterface(service.service_name, + service.service_factory, + browser()->BrowserMainThread()); + } +} + bool HeadlessWebContentsImpl::OpenURL(const GURL& url) { if (!url.is_valid()) return false; @@ -134,13 +176,18 @@ bool HeadlessWebContentsImpl::OpenURL(const GURL& url) { } void HeadlessWebContentsImpl::Close() { - browser_->DestroyWebContents(this); + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + browser_context()->DestroyWebContents(this); +} + +std::string HeadlessWebContentsImpl::GetDevToolsAgentHostId() { + return agent_host_->GetId(); } void HeadlessWebContentsImpl::AddObserver(Observer* observer) { DCHECK(observer_map_.find(observer) == observer_map_.end()); - observer_map_[observer] = base::WrapUnique( - new WebContentsObserverAdapter(web_contents_.get(), observer)); + observer_map_[observer] = base::MakeUnique<WebContentsObserverAdapter>( + web_contents_.get(), observer); } void HeadlessWebContentsImpl::RemoveObserver(Observer* observer) { @@ -154,8 +201,6 @@ HeadlessDevToolsTarget* HeadlessWebContentsImpl::GetDevToolsTarget() { } void HeadlessWebContentsImpl::AttachClient(HeadlessDevToolsClient* client) { - if (!agent_host_) - agent_host_ = content::DevToolsAgentHost::GetOrCreateFor(web_contents()); HeadlessDevToolsClientImpl::From(client)->AttachToHost(agent_host_.get()); } @@ -168,10 +213,18 @@ content::WebContents* HeadlessWebContentsImpl::web_contents() const { return web_contents_.get(); } -HeadlessWebContents::Builder::Builder(HeadlessBrowserImpl* browser) - : browser_(browser), - browser_context_( - browser->browser_main_parts()->default_browser_context()) {} +HeadlessBrowserImpl* HeadlessWebContentsImpl::browser() const { + return browser_context_->browser(); +} + +HeadlessBrowserContextImpl* HeadlessWebContentsImpl::browser_context() const { + return browser_context_; +} + +HeadlessWebContents::Builder::Builder( + HeadlessBrowserContextImpl* browser_context) + : browser_context_(browser_context), + window_size_(browser_context->options()->window_size()) {} HeadlessWebContents::Builder::~Builder() = default; @@ -189,14 +242,25 @@ HeadlessWebContents::Builder& HeadlessWebContents::Builder::SetWindowSize( return *this; } -HeadlessWebContents::Builder& HeadlessWebContents::Builder::SetBrowserContext( - HeadlessBrowserContext* browser_context) { - browser_context_ = browser_context; +HeadlessWebContents::Builder& HeadlessWebContents::Builder::AddMojoService( + const std::string& service_name, + const base::Callback<void(mojo::ScopedMessagePipeHandle)>& + service_factory) { + mojo_services_.emplace_back(service_name, service_factory); return *this; } HeadlessWebContents* HeadlessWebContents::Builder::Build() { - return browser_->CreateWebContents(this); + return browser_context_->CreateWebContents(this); } +HeadlessWebContents::Builder::MojoService::MojoService() {} + +HeadlessWebContents::Builder::MojoService::MojoService( + const std::string& service_name, + const base::Callback<void(mojo::ScopedMessagePipeHandle)>& service_factory) + : service_name(service_name), service_factory(service_factory) {} + +HeadlessWebContents::Builder::MojoService::~MojoService() {} + } // namespace headless diff --git a/chromium/headless/lib/browser/headless_web_contents_impl.h b/chromium/headless/lib/browser/headless_web_contents_impl.h index e0eec04afa2..43293847b35 100644 --- a/chromium/headless/lib/browser/headless_web_contents_impl.h +++ b/chromium/headless/lib/browser/headless_web_contents_impl.h @@ -5,12 +5,15 @@ #ifndef HEADLESS_LIB_BROWSER_HEADLESS_WEB_CONTENTS_IMPL_H_ #define HEADLESS_LIB_BROWSER_HEADLESS_WEB_CONTENTS_IMPL_H_ -#include "headless/public/headless_devtools_target.h" -#include "headless/public/headless_web_contents.h" - +#include <list> #include <memory> +#include <string> #include <unordered_map> +#include "content/public/browser/web_contents_observer.h" +#include "headless/public/headless_devtools_target.h" +#include "headless/public/headless_web_contents.h" + namespace aura { class Window; } @@ -31,19 +34,21 @@ class HeadlessBrowserImpl; class WebContentsObserverAdapter; class HeadlessWebContentsImpl : public HeadlessWebContents, - public HeadlessDevToolsTarget { + public HeadlessDevToolsTarget, + public content::WebContentsObserver { public: ~HeadlessWebContentsImpl() override; + static HeadlessWebContentsImpl* From(HeadlessWebContents* web_contents); + static std::unique_ptr<HeadlessWebContentsImpl> Create( HeadlessWebContents::Builder* builder, - aura::Window* parent_window, - HeadlessBrowserImpl* browser); + aura::Window* parent_window); // Takes ownership of |web_contents|. static std::unique_ptr<HeadlessWebContentsImpl> CreateFromWebContents( content::WebContents* web_contents, - HeadlessBrowserImpl* browser); + HeadlessBrowserContextImpl* browser_context); // HeadlessWebContents implementation: void AddObserver(Observer* observer) override; @@ -54,25 +59,36 @@ class HeadlessWebContentsImpl : public HeadlessWebContents, void AttachClient(HeadlessDevToolsClient* client) override; void DetachClient(HeadlessDevToolsClient* client) override; + // content::WebContentsObserver implementation: + void RenderFrameCreated(content::RenderFrameHost* render_frame_host) override; + content::WebContents* web_contents() const; bool OpenURL(const GURL& url); void Close() override; + std::string GetDevToolsAgentHostId(); + + HeadlessBrowserImpl* browser() const; + HeadlessBrowserContextImpl* browser_context() const; + private: // Takes ownership of |web_contents|. HeadlessWebContentsImpl(content::WebContents* web_contents, - HeadlessBrowserImpl* browser); + HeadlessBrowserContextImpl* browser_context); void InitializeScreen(aura::Window* parent_window, const gfx::Size& initial_size); + using MojoService = HeadlessWebContents::Builder::MojoService; + class Delegate; std::unique_ptr<Delegate> web_contents_delegate_; std::unique_ptr<content::WebContents> web_contents_; scoped_refptr<content::DevToolsAgentHost> agent_host_; + std::list<MojoService> mojo_services_; - HeadlessBrowserImpl* browser_; // Not owned. + HeadlessBrowserContextImpl* browser_context_; // Not owned. using ObserverMap = std::unordered_map<HeadlessWebContents::Observer*, diff --git a/chromium/headless/lib/browser/type_conversions_h.template b/chromium/headless/lib/browser/type_conversions_h.template index 286b0e5ea65..1c918ac9ea3 100644 --- a/chromium/headless/lib/browser/type_conversions_h.template +++ b/chromium/headless/lib/browser/type_conversions_h.template @@ -20,7 +20,7 @@ namespace internal { template <> struct FromValue<{{namespace}}::{{type.id}}> { static {{namespace}}::{{type.id}} Parse(const base::Value& value, ErrorReporter* errors) { - {% set default = namespace + '::' + type.id + '::' + type.enum[0] | dash_to_camelcase | camelcase_to_hacker_style | upper | mangle_enum %} + {% set default = namespace + '::' + type.id + '::' + type.enum[0] | sanitize_literal | dash_to_camelcase | camelcase_to_hacker_style | upper %} std::string string_value; if (!value.GetAsString(&string_value)) { errors->AddError("string enum value expected"); @@ -29,7 +29,7 @@ struct FromValue<{{namespace}}::{{type.id}}> { } {% for literal in type.enum %} if (string_value == "{{literal}}") - return {{namespace}}::{{type.id}}::{{literal | dash_to_camelcase | camelcase_to_hacker_style | upper | mangle_enum}}; + return {{namespace}}::{{type.id}}::{{literal | sanitize_literal | dash_to_camelcase | camelcase_to_hacker_style | upper }}; {% endfor %} errors->AddError("invalid enum value"); return {{default}}; @@ -40,7 +40,7 @@ template <typename T> std::unique_ptr<base::Value> ToValueImpl(const {{namespace}}::{{type.id}}& value, T*) { switch (value) { {% for literal in type.enum %} - case {{namespace}}::{{type.id}}::{{literal | dash_to_camelcase | camelcase_to_hacker_style | upper | mangle_enum}}: + case {{namespace}}::{{type.id}}::{{literal | sanitize_literal | dash_to_camelcase | camelcase_to_hacker_style | upper }}: return base::WrapUnique(new base::StringValue("{{literal}}")); {% endfor %} }; diff --git a/chromium/headless/lib/browser/types_h.template b/chromium/headless/lib/browser/types_h.template index 25b341d89dd..84b09cbeb24 100644 --- a/chromium/headless/lib/browser/types_h.template +++ b/chromium/headless/lib/browser/types_h.template @@ -39,7 +39,7 @@ namespace {{domain.domain | camelcase_to_hacker_style}} { {% if "enum" in type %} enum class {{type.id}} { {% for literal in type.enum %} - {{ literal | dash_to_camelcase | camelcase_to_hacker_style | upper | mangle_enum}}{{',' if not loop.last}} + {{ literal | sanitize_literal | dash_to_camelcase | camelcase_to_hacker_style | upper }}{{',' if not loop.last}} {% endfor %} }; @@ -70,7 +70,7 @@ class HEADLESS_EXPORT {{type.id}} { {% endif %} {% if property.optional %} bool Has{{property.name | to_title_case}}() const { return !!{{property.name | camelcase_to_hacker_style}}_; } - {{resolve_type(property).raw_return_type}} Get{{property.name | to_title_case}}() const { return {{resolve_type(property).to_raw_return_type % ("%s_.value()" % property.name | camelcase_to_hacker_style)}}; } + {{resolve_type(property).raw_return_type}} Get{{property.name | to_title_case}}() const { DCHECK(Has{{property.name | to_title_case}}()); return {{resolve_type(property).to_raw_return_type % ("%s_.value()" % property.name | camelcase_to_hacker_style)}}; } void Set{{property.name | to_title_case}}({{resolve_type(property).pass_type}} value) { {{property.name | camelcase_to_hacker_style}}_ = {{resolve_type(property).to_pass_type % 'value'}}; } {% else %} {{resolve_type(property).raw_return_type}} Get{{property.name | to_title_case}}() const { return {{resolve_type(property).to_raw_return_type % ("%s_" % property.name | camelcase_to_hacker_style)}}; } diff --git a/chromium/headless/lib/embedder_mojo_browsertest.cc b/chromium/headless/lib/embedder_mojo_browsertest.cc new file mode 100644 index 00000000000..a8338e04801 --- /dev/null +++ b/chromium/headless/lib/embedder_mojo_browsertest.cc @@ -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. + +#include <memory> +#include "base/optional.h" +#include "base/path_service.h" +#include "base/strings/stringprintf.h" +#include "base/threading/thread_restrictions.h" +#include "content/public/test/browser_test.h" +#include "headless/grit/headless_browsertest_resources.h" +#include "headless/lib/embedder_test.mojom.h" +#include "headless/public/domains/network.h" +#include "headless/public/domains/page.h" +#include "headless/public/domains/runtime.h" +#include "headless/public/headless_browser.h" +#include "headless/public/headless_devtools_client.h" +#include "headless/public/headless_devtools_target.h" +#include "headless/public/headless_web_contents.h" +#include "headless/test/headless_browser_test.h" +#include "mojo/public/cpp/bindings/binding_set.h" +#include "mojo/public/cpp/bindings/interface_ptr_set.h" +#include "mojo/public/cpp/bindings/interface_request.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/base/resource/resource_bundle.h" +#include "url/gurl.h" + +namespace headless { + +#define DEVTOOLS_CLIENT_TEST_F(TEST_FIXTURE_NAME) \ + IN_PROC_BROWSER_TEST_F(TEST_FIXTURE_NAME, RunAsyncTest) { RunTest(); } \ + class AsyncHeadlessBrowserTestNeedsSemicolon##TEST_FIXTURE_NAME {} + +// A test fixture which attaches a devtools client and registers a mojo +// interface before starting the test. +class EmbedderMojoTest : public HeadlessBrowserTest, + public HeadlessWebContents::Observer, + public embedder_test::TestEmbedderService { + public: + enum HttpPolicy { DEFAULT, ENABLE_HTTP }; + + EmbedderMojoTest() : EmbedderMojoTest(HttpPolicy::DEFAULT) {} + + explicit EmbedderMojoTest(HttpPolicy http_policy) + : browser_context_(nullptr), + web_contents_(nullptr), + devtools_client_(HeadlessDevToolsClient::Create()), + http_policy_(http_policy) {} + + ~EmbedderMojoTest() override {} + + void SetUpOnMainThread() override { + base::ThreadRestrictions::SetIOAllowed(true); + base::FilePath pak_path; + ASSERT_TRUE(PathService::Get(base::DIR_MODULE, &pak_path)); + pak_path = pak_path.AppendASCII("headless_browser_tests.pak"); + ResourceBundle::GetSharedInstance().AddDataPackFromPath( + pak_path, ui::SCALE_FACTOR_NONE); + } + + // HeadlessWebContentsObserver implementation: + void DevToolsTargetReady() override { + EXPECT_TRUE(web_contents_->GetDevToolsTarget()); + web_contents_->GetDevToolsTarget()->AttachClient(devtools_client_.get()); + + RunMojoTest(); + } + + virtual void RunMojoTest() = 0; + + virtual GURL GetInitialUrl() const { return GURL("about:blank"); } + + void OnEvalResult(std::unique_ptr<runtime::EvaluateResult> result) { + EXPECT_FALSE(result->HasExceptionDetails()) + << "JS exception: " << result->GetExceptionDetails()->GetText(); + if (result->HasExceptionDetails()) { + FinishAsynchronousTest(); + } + } + + protected: + void RunTest() { + // Using a pak file is idiomatic chromium style, but most embedders probably + // wouln't load the javascript bindings file this way. + std::string embedder_test_mojom_js = + ResourceBundle::GetSharedInstance() + .GetRawDataResource(IDR_HEADLESS_EMBEDDER_TEST_MOJOM_JS) + .as_string(); + + HeadlessBrowserContext::Builder builder = + browser()->CreateBrowserContextBuilder(); + builder.AddJsMojoBindings("headless/lib/embedder_test.mojom", + embedder_test_mojom_js); + if (http_policy_ == HttpPolicy::ENABLE_HTTP) { + builder.EnableUnsafeNetworkAccessWithMojoBindings(true); + } + if (host_resolver_rules_) { + builder.SetHostResolverRules(*host_resolver_rules_); + } + + browser_context_ = builder.Build(); + + web_contents_ = + browser_context_->CreateWebContentsBuilder() + .SetInitialURL(GURL(GetInitialUrl())) + .AddMojoService(base::Bind(&EmbedderMojoTest::CreateTestMojoService, + base::Unretained(this))) + .Build(); + + web_contents_->AddObserver(this); + RunAsynchronousTest(); + + web_contents_->GetDevToolsTarget()->DetachClient(devtools_client_.get()); + web_contents_->RemoveObserver(this); + web_contents_->Close(); + web_contents_ = nullptr; + + browser_context_->Close(); + browser_context_ = nullptr; + } + + void CreateTestMojoService( + mojo::InterfaceRequest<embedder_test::TestEmbedderService> request) { + test_embedder_mojo_bindings_.AddBinding(this, std::move(request)); + } + + HeadlessBrowserContext* browser_context_; + HeadlessWebContents* web_contents_; + std::unique_ptr<HeadlessDevToolsClient> devtools_client_; + + mojo::BindingSet<embedder_test::TestEmbedderService> + test_embedder_mojo_bindings_; + + HttpPolicy http_policy_; + base::Optional<std::string> host_resolver_rules_; +}; + +class MojoBindingsTest : public EmbedderMojoTest { + public: + void RunMojoTest() override { + devtools_client_->GetRuntime()->Evaluate( + "// Note define() defines a module in the mojo module dependency \n" + "// system. While we don't expose our module, the callback below only\n" + "// fires after the requested modules have been loaded. \n" + "define([ \n" + " 'headless/lib/embedder_test.mojom', \n" + " 'mojo/public/js/core', \n" + " 'mojo/public/js/router', \n" + " 'content/public/renderer/frame_interfaces', \n" + " ], function(embedderMojom, mojoCore, routerModule, \n" + " frameInterfaces) { \n" + " var testEmbedderService = \n" + " new embedderMojom.TestEmbedderService.proxyClass( \n" + " new routerModule.Router( \n" + " frameInterfaces.getInterface( \n" + " embedderMojom.TestEmbedderService.name))); \n" + " \n" + " // Send a message to the embedder! \n" + " testEmbedderService.returnTestResult('hello world'); \n" + "});", + base::Bind(&EmbedderMojoTest::OnEvalResult, base::Unretained(this))); + } + + // embedder_test::TestEmbedderService: + void ReturnTestResult(const std::string& result) override { + EXPECT_EQ("hello world", result); + FinishAsynchronousTest(); + } +}; + +DEVTOOLS_CLIENT_TEST_F(MojoBindingsTest); + +class MojoBindingsReinstalledAfterNavigation : public EmbedderMojoTest { + public: + MojoBindingsReinstalledAfterNavigation() + : EmbedderMojoTest(HttpPolicy::ENABLE_HTTP), seen_page_one_(false) { + EXPECT_TRUE(embedded_test_server()->Start()); + } + + void SetUpOnMainThread() override { + // We want to make sure bindings work across browser initiated cross-origin + // navigation, which is why we're setting up this fake tld. + host_resolver_rules_ = + base::StringPrintf("MAP not-an-actual-domain.tld 127.0.0.1:%d", + embedded_test_server()->host_port_pair().port()); + + EmbedderMojoTest::SetUpOnMainThread(); + } + + void RunMojoTest() override {} + + GURL GetInitialUrl() const override { + return embedded_test_server()->GetURL("/page_one.html"); + } + + // embedder_test::TestEmbedderService: + void ReturnTestResult(const std::string& result) override { + if (result == "page one") { + seen_page_one_ = true; + devtools_client_->GetPage()->Navigate( + "http://not-an-actual-domain.tld/page_two.html"); + } else { + EXPECT_TRUE(seen_page_one_); + EXPECT_EQ("page two", result); + FinishAsynchronousTest(); + } + } + + private: + bool seen_page_one_; +}; + +DEVTOOLS_CLIENT_TEST_F(MojoBindingsReinstalledAfterNavigation); + +class HttpDisabledByDefaultWhenMojoBindingsUsed : public EmbedderMojoTest, + network::Observer, + page::Observer { + public: + HttpDisabledByDefaultWhenMojoBindingsUsed() { + EXPECT_TRUE(embedded_test_server()->Start()); + } + + void RunMojoTest() override { + devtools_client_->GetNetwork()->AddObserver(this); + devtools_client_->GetNetwork()->Enable(); + } + + GURL GetInitialUrl() const override { + return embedded_test_server()->GetURL("/page_one.html"); + } + + void ReturnTestResult(const std::string& result) override { + FinishAsynchronousTest(); + FAIL() << "The HTTP page should not have been served and we should not have" + " recieved a mojo callback!"; + } + + void OnLoadingFailed(const network::LoadingFailedParams& params) override { + // The navigation should fail since HTTP requests are blackholed. + EXPECT_EQ(params.GetErrorText(), "net::ERR_FILE_NOT_FOUND"); + FinishAsynchronousTest(); + } +}; + +DEVTOOLS_CLIENT_TEST_F(HttpDisabledByDefaultWhenMojoBindingsUsed); + +} // namespace headless diff --git a/chromium/headless/lib/embedder_test.mojom b/chromium/headless/lib/embedder_test.mojom new file mode 100644 index 00000000000..553709a1986 --- /dev/null +++ b/chromium/headless/lib/embedder_test.mojom @@ -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. + +module embedder_test; + +// An example embedder Mojo service. +interface TestEmbedderService { + // Returns a test result to the embedder. + ReturnTestResult(string result); +}; diff --git a/chromium/headless/lib/headless_browser_browsertest.cc b/chromium/headless/lib/headless_browser_browsertest.cc index a6bbc8bf52d..77796ffe349 100644 --- a/chromium/headless/lib/headless_browser_browsertest.cc +++ b/chromium/headless/lib/headless_browser_browsertest.cc @@ -4,9 +4,13 @@ #include <memory> +#include "base/command_line.h" +#include "base/files/file_util.h" #include "base/memory/ptr_util.h" #include "base/strings/stringprintf.h" +#include "base/threading/thread_restrictions.h" #include "content/public/test/browser_test.h" +#include "headless/public/domains/network.h" #include "headless/public/domains/page.h" #include "headless/public/headless_browser.h" #include "headless/public/headless_devtools_client.h" @@ -14,31 +18,156 @@ #include "headless/public/headless_web_contents.h" #include "headless/test/headless_browser_test.h" #include "headless/test/test_protocol_handler.h" +#include "headless/test/test_url_request_job.h" +#include "net/base/registry_controlled_domains/registry_controlled_domain.h" +#include "net/cookies/cookie_store.h" #include "net/test/spawned_test_server/spawned_test_server.h" +#include "net/url_request/url_request_context.h" +#include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/gfx/geometry/size.h" +using testing::UnorderedElementsAre; + namespace headless { +IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, CreateAndDestroyBrowserContext) { + HeadlessBrowserContext* browser_context = + browser()->CreateBrowserContextBuilder().Build(); + + EXPECT_THAT(browser()->GetAllBrowserContexts(), + UnorderedElementsAre(browser_context)); + + browser_context->Close(); + + EXPECT_TRUE(browser()->GetAllBrowserContexts().empty()); +} + +IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, + CreateAndDoNotDestroyBrowserContext) { + HeadlessBrowserContext* browser_context = + browser()->CreateBrowserContextBuilder().Build(); + + EXPECT_THAT(browser()->GetAllBrowserContexts(), + UnorderedElementsAre(browser_context)); + + // We check that HeadlessBrowser correctly handles non-closed BrowserContexts. + // We can rely on Chromium DCHECKs to capture this. +} + IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, CreateAndDestroyWebContents) { + HeadlessBrowserContext* browser_context = + browser()->CreateBrowserContextBuilder().Build(); + HeadlessWebContents* web_contents = - browser()->CreateWebContentsBuilder().Build(); + browser_context->CreateWebContentsBuilder().Build(); EXPECT_TRUE(web_contents); - EXPECT_EQ(static_cast<size_t>(1), browser()->GetAllWebContents().size()); - EXPECT_EQ(web_contents, browser()->GetAllWebContents()[0]); + EXPECT_THAT(browser()->GetAllBrowserContexts(), + UnorderedElementsAre(browser_context)); + EXPECT_THAT(browser_context->GetAllWebContents(), + UnorderedElementsAre(web_contents)); + // TODO(skyostil): Verify viewport dimensions once we can. + web_contents->Close(); - EXPECT_TRUE(browser()->GetAllWebContents().empty()); + EXPECT_TRUE(browser_context->GetAllWebContents().empty()); + + browser_context->Close(); + + EXPECT_TRUE(browser()->GetAllBrowserContexts().empty()); +} + +IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, + WebContentsAreDestroyedWithContext) { + HeadlessBrowserContext* browser_context = + browser()->CreateBrowserContextBuilder().Build(); + + HeadlessWebContents* web_contents = + browser_context->CreateWebContentsBuilder().Build(); + EXPECT_TRUE(web_contents); + + EXPECT_THAT(browser()->GetAllBrowserContexts(), + UnorderedElementsAre(browser_context)); + EXPECT_THAT(browser_context->GetAllWebContents(), + UnorderedElementsAre(web_contents)); + + browser_context->Close(); + + EXPECT_TRUE(browser()->GetAllBrowserContexts().empty()); + + // If WebContents are not destroyed, Chromium DCHECKs will capture this. +} + +IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, CreateAndDoNotDestroyWebContents) { + HeadlessBrowserContext* browser_context = + browser()->CreateBrowserContextBuilder().Build(); + + HeadlessWebContents* web_contents = + browser_context->CreateWebContentsBuilder().Build(); + EXPECT_TRUE(web_contents); + + EXPECT_THAT(browser()->GetAllBrowserContexts(), + UnorderedElementsAre(browser_context)); + EXPECT_THAT(browser_context->GetAllWebContents(), + UnorderedElementsAre(web_contents)); + + // If WebContents are not destroyed, Chromium DCHECKs will capture this. +} + +IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, DestroyAndCreateTwoWebContents) { + HeadlessBrowserContext* browser_context1 = + browser()->CreateBrowserContextBuilder().Build(); + EXPECT_TRUE(browser_context1); + HeadlessWebContents* web_contents1 = + browser_context1->CreateWebContentsBuilder().Build(); + EXPECT_TRUE(web_contents1); + + EXPECT_THAT(browser()->GetAllBrowserContexts(), + UnorderedElementsAre(browser_context1)); + EXPECT_THAT(browser_context1->GetAllWebContents(), + UnorderedElementsAre(web_contents1)); + + HeadlessBrowserContext* browser_context2 = + browser()->CreateBrowserContextBuilder().Build(); + EXPECT_TRUE(browser_context2); + HeadlessWebContents* web_contents2 = + browser_context2->CreateWebContentsBuilder().Build(); + EXPECT_TRUE(web_contents2); + + EXPECT_THAT(browser()->GetAllBrowserContexts(), + UnorderedElementsAre(browser_context1, browser_context2)); + EXPECT_THAT(browser_context1->GetAllWebContents(), + UnorderedElementsAre(web_contents1)); + EXPECT_THAT(browser_context2->GetAllWebContents(), + UnorderedElementsAre(web_contents2)); + + browser_context1->Close(); + + EXPECT_THAT(browser()->GetAllBrowserContexts(), + UnorderedElementsAre(browser_context2)); + EXPECT_THAT(browser_context2->GetAllWebContents(), + UnorderedElementsAre(web_contents2)); + + browser_context2->Close(); + + EXPECT_TRUE(browser()->GetAllBrowserContexts().empty()); } IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, CreateWithBadURL) { GURL bad_url("not_valid"); + + HeadlessBrowserContext* browser_context = + browser()->CreateBrowserContextBuilder().Build(); + HeadlessWebContents* web_contents = - browser()->CreateWebContentsBuilder().SetInitialURL(bad_url).Build(); + browser_context->CreateWebContentsBuilder() + .SetInitialURL(bad_url) + .Build(); + EXPECT_FALSE(web_contents); - EXPECT_TRUE(browser()->GetAllWebContents().empty()); + EXPECT_TRUE(browser_context->GetAllWebContents().empty()); } class HeadlessBrowserTestWithProxy : public HeadlessBrowserTest { @@ -66,42 +195,48 @@ class HeadlessBrowserTestWithProxy : public HeadlessBrowserTest { }; IN_PROC_BROWSER_TEST_F(HeadlessBrowserTestWithProxy, SetProxyServer) { - HeadlessBrowser::Options::Builder builder; - builder.SetProxyServer(proxy_server()->host_port_pair()); - SetBrowserOptions(builder.Build()); + HeadlessBrowserContext* browser_context = + browser() + ->CreateBrowserContextBuilder() + .SetProxyServer(proxy_server()->host_port_pair()) + .Build(); // Load a page which doesn't actually exist, but for which the our proxy // returns valid content anyway. - // - // TODO(altimin): Currently this construction does not serve hello.html - // from headless/test/data as expected. We should fix this. HeadlessWebContents* web_contents = - browser() - ->CreateWebContentsBuilder() + browser_context->CreateWebContentsBuilder() .SetInitialURL(GURL("http://not-an-actual-domain.tld/hello.html")) .Build(); EXPECT_TRUE(WaitForLoad(web_contents)); - EXPECT_EQ(static_cast<size_t>(1), browser()->GetAllWebContents().size()); - EXPECT_EQ(web_contents, browser()->GetAllWebContents()[0]); + EXPECT_THAT(browser()->GetAllBrowserContexts(), + UnorderedElementsAre(browser_context)); + EXPECT_THAT(browser_context->GetAllWebContents(), + UnorderedElementsAre(web_contents)); web_contents->Close(); - EXPECT_TRUE(browser()->GetAllWebContents().empty()); + EXPECT_TRUE(browser_context->GetAllWebContents().empty()); } IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, SetHostResolverRules) { EXPECT_TRUE(embedded_test_server()->Start()); - HeadlessBrowser::Options::Builder builder; - builder.SetHostResolverRules( + + std::string host_resolver_rules = base::StringPrintf("MAP not-an-actual-domain.tld 127.0.0.1:%d", - embedded_test_server()->host_port_pair().port())); - SetBrowserOptions(builder.Build()); + embedded_test_server()->host_port_pair().port()); + + HeadlessBrowserContext* browser_context = + browser() + ->CreateBrowserContextBuilder() + .SetHostResolverRules(host_resolver_rules) + .Build(); // Load a page which doesn't actually exist, but which is turned into a valid // address by our host resolver rules. HeadlessWebContents* web_contents = - browser() - ->CreateWebContentsBuilder() + browser_context->CreateWebContentsBuilder() .SetInitialURL(GURL("http://not-an-actual-domain.tld/hello.html")) .Build(); + EXPECT_TRUE(web_contents); + EXPECT_TRUE(WaitForLoad(web_contents)); } @@ -109,19 +244,21 @@ IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, HttpProtocolHandler) { const std::string kResponseBody = "<p>HTTP response body</p>"; ProtocolHandlerMap protocol_handlers; protocol_handlers[url::kHttpScheme] = - base::WrapUnique(new TestProtocolHandler(kResponseBody)); + base::MakeUnique<TestProtocolHandler>(kResponseBody); - HeadlessBrowser::Options::Builder builder; - builder.SetProtocolHandlers(std::move(protocol_handlers)); - SetBrowserOptions(builder.Build()); + HeadlessBrowserContext* browser_context = + browser() + ->CreateBrowserContextBuilder() + .SetProtocolHandlers(std::move(protocol_handlers)) + .Build(); // Load a page which doesn't actually exist, but which is fetched by our // custom protocol handler. HeadlessWebContents* web_contents = - browser() - ->CreateWebContentsBuilder() + browser_context->CreateWebContentsBuilder() .SetInitialURL(GURL("http://not-an-actual-domain.tld/hello.html")) .Build(); + EXPECT_TRUE(web_contents); EXPECT_TRUE(WaitForLoad(web_contents)); std::string inner_html; @@ -136,19 +273,21 @@ IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, HttpsProtocolHandler) { const std::string kResponseBody = "<p>HTTPS response body</p>"; ProtocolHandlerMap protocol_handlers; protocol_handlers[url::kHttpsScheme] = - base::WrapUnique(new TestProtocolHandler(kResponseBody)); + base::MakeUnique<TestProtocolHandler>(kResponseBody); - HeadlessBrowser::Options::Builder builder; - builder.SetProtocolHandlers(std::move(protocol_handlers)); - SetBrowserOptions(builder.Build()); + HeadlessBrowserContext* browser_context = + browser() + ->CreateBrowserContextBuilder() + .SetProtocolHandlers(std::move(protocol_handlers)) + .Build(); // Load a page which doesn't actually exist, but which is fetched by our // custom protocol handler. HeadlessWebContents* web_contents = - browser() - ->CreateWebContentsBuilder() + browser_context->CreateWebContentsBuilder() .SetInitialURL(GURL("https://not-an-actual-domain.tld/hello.html")) .Build(); + EXPECT_TRUE(web_contents); EXPECT_TRUE(WaitForLoad(web_contents)); std::string inner_html; @@ -159,4 +298,348 @@ IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, HttpsProtocolHandler) { EXPECT_EQ(kResponseBody, inner_html); } +IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, WebGLSupported) { + HeadlessBrowserContext* browser_context = + browser()->CreateBrowserContextBuilder().Build(); + + HeadlessWebContents* web_contents = + browser_context->CreateWebContentsBuilder().Build(); + + bool webgl_supported; + EXPECT_TRUE( + EvaluateScript(web_contents, + "(document.createElement('canvas').getContext('webgl')" + " instanceof WebGLRenderingContext)") + ->GetResult() + ->GetValue() + ->GetAsBoolean(&webgl_supported)); + EXPECT_TRUE(webgl_supported); +} + +IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, DefaultSizes) { + HeadlessBrowserContext* browser_context = + browser()->CreateBrowserContextBuilder().Build(); + + HeadlessWebContents* web_contents = + browser_context->CreateWebContentsBuilder().Build(); + + HeadlessBrowser::Options::Builder builder; + const HeadlessBrowser::Options kDefaultOptions = builder.Build(); + + int screen_width; + int screen_height; + int window_width; + int window_height; + + EXPECT_TRUE(EvaluateScript(web_contents, "screen.width") + ->GetResult() + ->GetValue() + ->GetAsInteger(&screen_width)); + EXPECT_TRUE(EvaluateScript(web_contents, "screen.height") + ->GetResult() + ->GetValue() + ->GetAsInteger(&screen_height)); + EXPECT_TRUE(EvaluateScript(web_contents, "window.innerWidth") + ->GetResult() + ->GetValue() + ->GetAsInteger(&window_width)); + EXPECT_TRUE(EvaluateScript(web_contents, "window.innerHeight") + ->GetResult() + ->GetValue() + ->GetAsInteger(&window_height)); + + EXPECT_EQ(kDefaultOptions.window_size.width(), screen_width); + EXPECT_EQ(kDefaultOptions.window_size.height(), screen_height); + EXPECT_EQ(kDefaultOptions.window_size.width(), window_width); + EXPECT_EQ(kDefaultOptions.window_size.height(), window_height); +} + +namespace { + +// True if the request method is "safe" (per section 4.2.1 of RFC 7231). +bool IsMethodSafe(const std::string& method) { + return method == "GET" || method == "HEAD" || method == "OPTIONS" || + method == "TRACE"; +} + +class ProtocolHandlerWithCookies + : public net::URLRequestJobFactory::ProtocolHandler { + public: + ProtocolHandlerWithCookies(net::CookieList* sent_cookies); + ~ProtocolHandlerWithCookies() override {} + + net::URLRequestJob* MaybeCreateJob( + net::URLRequest* request, + net::NetworkDelegate* network_delegate) const override; + + private: + net::CookieList* sent_cookies_; // Not owned. + + DISALLOW_COPY_AND_ASSIGN(ProtocolHandlerWithCookies); +}; + +class URLRequestJobWithCookies : public TestURLRequestJob { + public: + URLRequestJobWithCookies(net::URLRequest* request, + net::NetworkDelegate* network_delegate, + net::CookieList* sent_cookies); + ~URLRequestJobWithCookies() override {} + + // net::URLRequestJob implementation: + void Start() override; + + private: + void SaveCookiesAndStart(const net::CookieList& cookie_list); + + net::CookieList* sent_cookies_; // Not owned. + base::WeakPtrFactory<URLRequestJobWithCookies> weak_factory_; + DISALLOW_COPY_AND_ASSIGN(URLRequestJobWithCookies); +}; + +ProtocolHandlerWithCookies::ProtocolHandlerWithCookies( + net::CookieList* sent_cookies) + : sent_cookies_(sent_cookies) {} + +net::URLRequestJob* ProtocolHandlerWithCookies::MaybeCreateJob( + net::URLRequest* request, + net::NetworkDelegate* network_delegate) const { + return new URLRequestJobWithCookies(request, network_delegate, sent_cookies_); +} + +URLRequestJobWithCookies::URLRequestJobWithCookies( + net::URLRequest* request, + net::NetworkDelegate* network_delegate, + net::CookieList* sent_cookies) + // Return an empty response for every request. + : TestURLRequestJob(request, network_delegate, ""), + sent_cookies_(sent_cookies), + weak_factory_(this) {} + +void URLRequestJobWithCookies::Start() { + net::CookieStore* cookie_store = request_->context()->cookie_store(); + net::CookieOptions options; + options.set_include_httponly(); + + // See net::URLRequestHttpJob::AddCookieHeaderAndStart(). + url::Origin requested_origin(request_->url()); + url::Origin site_for_cookies(request_->first_party_for_cookies()); + + if (net::registry_controlled_domains::SameDomainOrHost( + requested_origin, site_for_cookies, + net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES)) { + if (net::registry_controlled_domains::SameDomainOrHost( + requested_origin, request_->initiator(), + net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES)) { + options.set_same_site_cookie_mode( + net::CookieOptions::SameSiteCookieMode::INCLUDE_STRICT_AND_LAX); + } else if (IsMethodSafe(request_->method())) { + options.set_same_site_cookie_mode( + net::CookieOptions::SameSiteCookieMode::INCLUDE_LAX); + } + } + cookie_store->GetCookieListWithOptionsAsync( + request_->url(), options, + base::Bind(&URLRequestJobWithCookies::SaveCookiesAndStart, + weak_factory_.GetWeakPtr())); +} + +void URLRequestJobWithCookies::SaveCookiesAndStart( + const net::CookieList& cookie_list) { + *sent_cookies_ = cookie_list; + NotifyHeadersComplete(); +} + +} // namespace + +IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, ReadCookiesInProtocolHandler) { + net::CookieList sent_cookies; + ProtocolHandlerMap protocol_handlers; + protocol_handlers[url::kHttpsScheme] = + base::MakeUnique<ProtocolHandlerWithCookies>(&sent_cookies); + + HeadlessBrowserContext* browser_context = + browser() + ->CreateBrowserContextBuilder() + .SetProtocolHandlers(std::move(protocol_handlers)) + .Build(); + + HeadlessWebContents* web_contents = + browser_context->CreateWebContentsBuilder() + .SetInitialURL(GURL("https://example.com/cookie.html")) + .Build(); + EXPECT_TRUE(WaitForLoad(web_contents)); + + // The first load has no cookies. + EXPECT_EQ(0u, sent_cookies.size()); + + // Set a cookie and reload the page. + EXPECT_FALSE(EvaluateScript( + web_contents, + "document.cookie = 'shape=oblong', window.location.reload()") + ->HasExceptionDetails()); + EXPECT_TRUE(WaitForLoad(web_contents)); + + // We should have sent the cookie this time. + EXPECT_EQ(1u, sent_cookies.size()); + EXPECT_EQ("shape", sent_cookies[0].Name()); + EXPECT_EQ("oblong", sent_cookies[0].Value()); +} + +namespace { + +class CookieSetter { + public: + CookieSetter(HeadlessBrowserTest* browser_test, + HeadlessWebContents* web_contents, + std::unique_ptr<network::SetCookieParams> set_cookie_params) + : browser_test_(browser_test), + web_contents_(web_contents), + devtools_client_(HeadlessDevToolsClient::Create()) { + web_contents_->GetDevToolsTarget()->AttachClient(devtools_client_.get()); + devtools_client_->GetNetwork()->GetExperimental()->SetCookie( + std::move(set_cookie_params), + base::Bind(&CookieSetter::OnSetCookieResult, base::Unretained(this))); + } + + ~CookieSetter() { + web_contents_->GetDevToolsTarget()->DetachClient(devtools_client_.get()); + } + + void OnSetCookieResult(std::unique_ptr<network::SetCookieResult> result) { + result_ = std::move(result); + browser_test_->FinishAsynchronousTest(); + } + + std::unique_ptr<network::SetCookieResult> TakeResult() { + return std::move(result_); + } + + private: + HeadlessBrowserTest* browser_test_; // Not owned. + HeadlessWebContents* web_contents_; // Not owned. + std::unique_ptr<HeadlessDevToolsClient> devtools_client_; + + std::unique_ptr<network::SetCookieResult> result_; + + DISALLOW_COPY_AND_ASSIGN(CookieSetter); +}; + +} // namespace + +IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, SetCookiesWithDevTools) { + net::CookieList sent_cookies; + ProtocolHandlerMap protocol_handlers; + protocol_handlers[url::kHttpsScheme] = + base::WrapUnique(new ProtocolHandlerWithCookies(&sent_cookies)); + + HeadlessBrowserContext* browser_context = + browser() + ->CreateBrowserContextBuilder() + .SetProtocolHandlers(std::move(protocol_handlers)) + .Build(); + + HeadlessWebContents* web_contents = + browser_context->CreateWebContentsBuilder() + .SetInitialURL(GURL("https://example.com/cookie.html")) + .Build(); + EXPECT_TRUE(WaitForLoad(web_contents)); + + // The first load has no cookies. + EXPECT_EQ(0u, sent_cookies.size()); + + // Set some cookies. + { + std::unique_ptr<network::SetCookieParams> set_cookie_params = + network::SetCookieParams::Builder() + .SetUrl("https://example.com") + .SetName("shape") + .SetValue("oblong") + .Build(); + CookieSetter cookie_setter(this, web_contents, + std::move(set_cookie_params)); + RunAsynchronousTest(); + std::unique_ptr<network::SetCookieResult> result = + cookie_setter.TakeResult(); + EXPECT_TRUE(result->GetSuccess()); + } + { + // Try setting all the fields so we notice if the protocol for any of them + // changes. + std::unique_ptr<network::SetCookieParams> set_cookie_params = + network::SetCookieParams::Builder() + .SetUrl("https://other.com") + .SetName("shape") + .SetValue("trapezoid") + .SetDomain("other.com") + .SetPath("") + .SetSecure(true) + .SetHttpOnly(true) + .SetSameSite(network::CookieSameSite::STRICT) + .SetExpirationDate(0) + .Build(); + CookieSetter cookie_setter(this, web_contents, + std::move(set_cookie_params)); + RunAsynchronousTest(); + std::unique_ptr<network::SetCookieResult> result = + cookie_setter.TakeResult(); + EXPECT_TRUE(result->GetSuccess()); + } + + // Reload the page. + EXPECT_FALSE(EvaluateScript(web_contents, "window.location.reload();") + ->HasExceptionDetails()); + EXPECT_TRUE(WaitForLoad(web_contents)); + + // We should have sent the matching cookies this time. + EXPECT_EQ(1u, sent_cookies.size()); + EXPECT_EQ("shape", sent_cookies[0].Name()); + EXPECT_EQ("oblong", sent_cookies[0].Value()); +} + +// TODO(skyostil): This test currently relies on being able to run a shell +// script. +#if defined(OS_POSIX) +#define MAYBE_RendererCommandPrefixTest RendererCommandPrefixTest +#else +#define MAYBE_RendererCommandPrefixTest DISABLED_RendererCommandPrefixTest +#endif // defined(OS_POSIX) +IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, MAYBE_RendererCommandPrefixTest) { + base::ThreadRestrictions::SetIOAllowed(true); + base::FilePath launcher_stamp; + base::CreateTemporaryFile(&launcher_stamp); + + base::FilePath launcher_script; + FILE* launcher_file = base::CreateAndOpenTemporaryFile(&launcher_script); + fprintf(launcher_file, "#!/bin/sh\n"); + fprintf(launcher_file, "echo $@ > %s\n", launcher_stamp.value().c_str()); + fprintf(launcher_file, "exec $@\n"); + fclose(launcher_file); + base::SetPosixFilePermissions(launcher_script, + base::FILE_PERMISSION_READ_BY_USER | + base::FILE_PERMISSION_EXECUTE_BY_USER); + + base::CommandLine::ForCurrentProcess()->AppendSwitch("--no-sandbox"); + base::CommandLine::ForCurrentProcess()->AppendSwitchASCII( + "--renderer-cmd-prefix", launcher_script.value()); + + EXPECT_TRUE(embedded_test_server()->Start()); + + HeadlessBrowserContext* browser_context = + browser()->CreateBrowserContextBuilder().Build(); + + HeadlessWebContents* web_contents = + browser_context->CreateWebContentsBuilder() + .SetInitialURL(embedded_test_server()->GetURL("/hello.html")) + .Build(); + EXPECT_TRUE(WaitForLoad(web_contents)); + + // Make sure the launcher was invoked when starting the renderer. + std::string stamp; + EXPECT_TRUE(base::ReadFileToString(launcher_stamp, &stamp)); + EXPECT_GE(stamp.find("--type=renderer"), 0u); + + base::DeleteFile(launcher_script, false); + base::DeleteFile(launcher_stamp, false); +} + } // namespace headless diff --git a/chromium/headless/lib/headless_browser_context_browsertest.cc b/chromium/headless/lib/headless_browser_context_browsertest.cc index 34490bb6284..49ab195c094 100644 --- a/chromium/headless/lib/headless_browser_context_browsertest.cc +++ b/chromium/headless/lib/headless_browser_context_browsertest.cc @@ -4,9 +4,14 @@ #include <memory> +#include "base/files/scoped_temp_dir.h" #include "base/memory/ptr_util.h" #include "base/strings/stringprintf.h" +#include "base/threading/thread_restrictions.h" +#include "content/public/browser/render_view_host.h" +#include "content/public/browser/web_contents.h" #include "content/public/test/browser_test.h" +#include "headless/lib/browser/headless_web_contents_impl.h" #include "headless/public/domains/runtime.h" #include "headless/public/headless_browser.h" #include "headless/public/headless_browser_context.h" @@ -46,7 +51,8 @@ class HeadlessBrowserContextIsolationTest : public HeadlessAsyncDevTooledBrowserTest { public: HeadlessBrowserContextIsolationTest() - : web_contents2_(nullptr), + : browser_context_(nullptr), + web_contents2_(nullptr), devtools_client2_(HeadlessDevToolsClient::Create()) { EXPECT_TRUE(embedded_test_server()->Start()); } @@ -55,10 +61,7 @@ class HeadlessBrowserContextIsolationTest void DevToolsTargetReady() override { if (!web_contents2_) { browser_context_ = browser()->CreateBrowserContextBuilder().Build(); - web_contents2_ = browser() - ->CreateWebContentsBuilder() - .SetBrowserContext(browser_context_.get()) - .Build(); + web_contents2_ = browser_context_->CreateWebContentsBuilder().Build(); web_contents2_->AddObserver(this); return; } @@ -143,12 +146,12 @@ class HeadlessBrowserContextIsolationTest void FinishTest() { web_contents2_->RemoveObserver(this); web_contents2_->Close(); - browser_context_.reset(); + browser_context_->Close(); FinishAsynchronousTest(); } private: - std::unique_ptr<HeadlessBrowserContext> browser_context_; + HeadlessBrowserContext* browser_context_; HeadlessWebContents* web_contents2_; std::unique_ptr<HeadlessDevToolsClient> devtools_client2_; std::unique_ptr<LoadObserver> load_observer_; @@ -160,20 +163,18 @@ IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, ContextProtocolHandler) { const std::string kResponseBody = "<p>HTTP response body</p>"; ProtocolHandlerMap protocol_handlers; protocol_handlers[url::kHttpScheme] = - base::WrapUnique(new TestProtocolHandler(kResponseBody)); + base::MakeUnique<TestProtocolHandler>(kResponseBody); // Load a page which doesn't actually exist, but which is fetched by our // custom protocol handler. - std::unique_ptr<HeadlessBrowserContext> browser_context = + HeadlessBrowserContext* browser_context = browser() ->CreateBrowserContextBuilder() .SetProtocolHandlers(std::move(protocol_handlers)) .Build(); HeadlessWebContents* web_contents = - browser() - ->CreateWebContentsBuilder() + browser_context->CreateWebContentsBuilder() .SetInitialURL(GURL("http://not-an-actual-domain.tld/hello.html")) - .SetBrowserContext(browser_context.get()) .Build(); EXPECT_TRUE(WaitForLoad(web_contents)); @@ -185,12 +186,14 @@ IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, ContextProtocolHandler) { EXPECT_EQ(kResponseBody, inner_html); web_contents->Close(); - // Loading the same non-existent page using a tab with the default context + HeadlessBrowserContext* another_browser_context = + browser()->CreateBrowserContextBuilder().Build(); + + // Loading the same non-existent page using a tab with a different context // should not work since the protocol handler only exists on the custom // context. web_contents = - browser() - ->CreateWebContentsBuilder() + another_browser_context->CreateWebContentsBuilder() .SetInitialURL(GURL("http://not-an-actual-domain.tld/hello.html")) .Build(); EXPECT_TRUE(WaitForLoad(web_contents)); @@ -202,4 +205,95 @@ IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, ContextProtocolHandler) { web_contents->Close(); } +IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, UserDataDir) { + // We do not want to bother with posting tasks to create a temp dir. + // Just allow IO from main thread for now. + base::ThreadRestrictions::SetIOAllowed(true); + + EXPECT_TRUE(embedded_test_server()->Start()); + + base::ScopedTempDir user_data_dir; + ASSERT_TRUE(user_data_dir.CreateUniqueTempDir()); + + // Newly created temp directory should be empty. + EXPECT_TRUE(base::IsDirectoryEmpty(user_data_dir.GetPath())); + + HeadlessBrowserContext* browser_context = + browser() + ->CreateBrowserContextBuilder() + .SetUserDataDir(user_data_dir.GetPath()) + .SetIncognitoMode(false) + .Build(); + + HeadlessWebContents* web_contents = + browser_context->CreateWebContentsBuilder() + .SetInitialURL(embedded_test_server()->GetURL("/hello.html")) + .Build(); + + EXPECT_TRUE(WaitForLoad(web_contents)); + + // Something should be written to this directory. + // If it is not the case, more complex page may be needed. + // ServiceWorkers may be a good option. + EXPECT_FALSE(base::IsDirectoryEmpty(user_data_dir.GetPath())); +} + +IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, IncognitoMode) { + // We do not want to bother with posting tasks to create a temp dir. + // Just allow IO from main thread for now. + base::ThreadRestrictions::SetIOAllowed(true); + + EXPECT_TRUE(embedded_test_server()->Start()); + + base::ScopedTempDir user_data_dir; + ASSERT_TRUE(user_data_dir.CreateUniqueTempDir()); + + // Newly created temp directory should be empty. + EXPECT_TRUE(base::IsDirectoryEmpty(user_data_dir.GetPath())); + + HeadlessBrowserContext* browser_context = + browser() + ->CreateBrowserContextBuilder() + .SetUserDataDir(user_data_dir.GetPath()) + .SetIncognitoMode(true) + .Build(); + + HeadlessWebContents* web_contents = + browser_context->CreateWebContentsBuilder() + .SetInitialURL(embedded_test_server()->GetURL("/hello.html")) + .Build(); + + EXPECT_TRUE(WaitForLoad(web_contents)); + + // Similar to test above, but now we are in incognito mode, + // so nothing should be written to this directory. + EXPECT_TRUE(base::IsDirectoryEmpty(user_data_dir.GetPath())); +} + +IN_PROC_BROWSER_TEST_F(HeadlessBrowserTest, ContextWebPreferences) { + // By default, hide_scrollbars should be false. + EXPECT_FALSE(WebPreferences().hide_scrollbars); + + // Set hide_scrollbars preference to true for a new BrowserContext. + HeadlessBrowserContext* browser_context = + browser() + ->CreateBrowserContextBuilder() + .SetOverrideWebPreferencesCallback( + base::Bind([](headless::WebPreferences* preferences) { + preferences->hide_scrollbars = true; + })) + .Build(); + HeadlessWebContents* web_contents = + browser_context->CreateWebContentsBuilder() + .SetInitialURL(GURL("about:blank")) + .Build(); + + // Verify that the preference takes effect. + HeadlessWebContentsImpl* contents_impl = + HeadlessWebContentsImpl::From(web_contents); + EXPECT_TRUE(contents_impl->web_contents() + ->GetRenderViewHost() + ->GetWebkitPreferences().hide_scrollbars); +} + } // namespace headless diff --git a/chromium/headless/lib/headless_browsertest_resource_ids b/chromium/headless/lib/headless_browsertest_resource_ids new file mode 100644 index 00000000000..fad4a66f794 --- /dev/null +++ b/chromium/headless/lib/headless_browsertest_resource_ids @@ -0,0 +1,7 @@ +{ + "SRCDIR": "../..", + + "headless/lib/headless_browsertest_resources.grd": { + "includes": [31000], + } +} diff --git a/chromium/headless/lib/headless_browsertest_resources.grd b/chromium/headless/lib/headless_browsertest_resources.grd new file mode 100644 index 00000000000..db803c1609d --- /dev/null +++ b/chromium/headless/lib/headless_browsertest_resources.grd @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<grit latest_public_release="0" current_release="1"> + <outputs> + <output filename="grit/headless_browsertest_resources.h" type="rc_header"> + <emit emit_type='prepend'></emit> + </output> + <output filename="headless_browsertest_resources.pak" type="data_package" /> + </outputs> + <translations /> + <release seq="1"> + <includes> + <include name="IDR_HEADLESS_EMBEDDER_TEST_MOJOM_JS" file="${mojom_root}\headless\lib\embedder_test.mojom.js" use_base_dir="false" type="BINDATA" /> + </includes> + </release> +</grit> diff --git a/chromium/headless/lib/headless_content_main_delegate.cc b/chromium/headless/lib/headless_content_main_delegate.cc index 97d04130d77..193169ff8a0 100644 --- a/chromium/headless/lib/headless_content_main_delegate.cc +++ b/chromium/headless/lib/headless_content_main_delegate.cc @@ -15,6 +15,7 @@ #include "headless/lib/renderer/headless_content_renderer_client.h" #include "headless/lib/utility/headless_content_utility_client.h" #include "ui/base/resource/resource_bundle.h" +#include "ui/gl/gl_switches.h" #include "ui/ozone/public/ozone_switches.h" namespace headless { @@ -44,12 +45,19 @@ bool HeadlessContentMainDelegate::BasicStartupComplete(int* exit_code) { if (browser_->options()->single_process_mode) command_line->AppendSwitch(switches::kSingleProcess); + if (browser_->options()->disable_sandbox) + command_line->AppendSwitch(switches::kNoSandbox); + // The headless backend is automatically chosen for a headless build, but also // adding it here allows us to run in a non-headless build too. command_line->AppendSwitchASCII(switches::kOzonePlatform, "headless"); - // TODO(skyostil): Investigate using Mesa/SwiftShader for output. - command_line->AppendSwitch(switches::kDisableGpu); + if (!browser_->options()->gl_implementation.empty()) { + command_line->AppendSwitchASCII(switches::kUseGL, + browser_->options()->gl_implementation); + } else { + command_line->AppendSwitch(switches::kDisableGpu); + } SetContentClient(&content_client_); return false; diff --git a/chromium/headless/lib/headless_devtools_client_browsertest.cc b/chromium/headless/lib/headless_devtools_client_browsertest.cc index 8b51fecac51..5188e74c404 100644 --- a/chromium/headless/lib/headless_devtools_client_browsertest.cc +++ b/chromium/headless/lib/headless_devtools_client_browsertest.cc @@ -4,18 +4,48 @@ #include <memory> +#include "base/json/json_reader.h" +#include "content/public/browser/render_widget_host_view.h" +#include "content/public/browser/web_contents.h" #include "content/public/test/browser_test.h" +#include "headless/lib/browser/headless_web_contents_impl.h" +#include "headless/public/domains/browser.h" +#include "headless/public/domains/emulation.h" #include "headless/public/domains/network.h" #include "headless/public/domains/page.h" #include "headless/public/domains/runtime.h" #include "headless/public/headless_browser.h" #include "headless/public/headless_devtools_client.h" +#include "headless/public/headless_devtools_target.h" #include "headless/test/headless_browser_test.h" #include "testing/gtest/include/gtest/gtest.h" #include "url/gurl.h" +#define EXPECT_SIZE_EQ(expected, actual) \ + do { \ + EXPECT_EQ((expected).width(), (actual).width()); \ + EXPECT_EQ((expected).height(), (actual).height()); \ + } while (false) + namespace headless { +namespace { + +std::vector<HeadlessWebContents*> GetAllWebContents(HeadlessBrowser* browser) { + std::vector<HeadlessWebContents*> result; + + for (HeadlessBrowserContext* browser_context : + browser->GetAllBrowserContexts()) { + std::vector<HeadlessWebContents*> web_contents = + browser_context->GetAllWebContents(); + result.insert(result.end(), web_contents.begin(), web_contents.end()); + } + + return result; +} + +} // namespace + class HeadlessDevToolsClientNavigationTest : public HeadlessAsyncDevTooledBrowserTest, page::ExperimentalObserver { @@ -155,8 +185,17 @@ class HeadlessDevToolsClientExperimentalTest void RunDevTooledTest() override { EXPECT_TRUE(embedded_test_server()->Start()); // Check that experimental commands require parameter objects. - devtools_client_->GetRuntime()->GetExperimental()->Run( - runtime::RunParams::Builder().Build()); + devtools_client_->GetRuntime() + ->GetExperimental() + ->SetCustomObjectFormatterEnabled( + runtime::SetCustomObjectFormatterEnabledParams::Builder() + .SetEnabled(false) + .Build()); + + // Check that a previously experimental command which takes no parameters + // still works by giving it a parameter object. + devtools_client_->GetRuntime()->GetExperimental()->RunIfWaitingForDebugger( + runtime::RunIfWaitingForDebuggerParams::Builder().Build()); devtools_client_->GetPage()->GetExperimental()->AddObserver(this); devtools_client_->GetPage()->Enable(); @@ -166,10 +205,452 @@ class HeadlessDevToolsClientExperimentalTest void OnFrameStoppedLoading( const page::FrameStoppedLoadingParams& params) override { - FinishAsynchronousTest(); + // Check that a non-experimental command which has no return value can be + // called with a void() callback. + devtools_client_->GetPage()->Reload( + page::ReloadParams::Builder().Build(), + base::Bind(&HeadlessDevToolsClientExperimentalTest::OnReloadStarted, + base::Unretained(this))); } + + void OnReloadStarted() { FinishAsynchronousTest(); } }; HEADLESS_ASYNC_DEVTOOLED_TEST_F(HeadlessDevToolsClientExperimentalTest); +class BrowserDomainCreateAndDeletePageTest + : public HeadlessAsyncDevTooledBrowserTest { + void RunDevTooledTest() override { + EXPECT_TRUE(embedded_test_server()->Start()); + + EXPECT_EQ(1u, GetAllWebContents(browser()).size()); + + devtools_client_->GetBrowser()->GetExperimental()->CreateTarget( + browser::CreateTargetParams::Builder() + .SetUrl(embedded_test_server()->GetURL("/hello.html").spec()) + .SetWidth(1) + .SetHeight(1) + .Build(), + base::Bind(&BrowserDomainCreateAndDeletePageTest::OnCreateTargetResult, + base::Unretained(this))); + } + + void OnCreateTargetResult( + std::unique_ptr<browser::CreateTargetResult> result) { + EXPECT_EQ(2u, GetAllWebContents(browser()).size()); + + HeadlessWebContentsImpl* contents = HeadlessWebContentsImpl::From( + browser()->GetWebContentsForDevToolsAgentHostId(result->GetTargetId())); + EXPECT_SIZE_EQ(gfx::Size(1, 1), contents->web_contents() + ->GetRenderWidgetHostView() + ->GetViewBounds() + .size()); + + devtools_client_->GetBrowser()->GetExperimental()->CloseTarget( + browser::CloseTargetParams::Builder() + .SetTargetId(result->GetTargetId()) + .Build(), + base::Bind(&BrowserDomainCreateAndDeletePageTest::OnCloseTargetResult, + base::Unretained(this))); + } + + void OnCloseTargetResult(std::unique_ptr<browser::CloseTargetResult> result) { + EXPECT_TRUE(result->GetSuccess()); + EXPECT_EQ(1u, GetAllWebContents(browser()).size()); + FinishAsynchronousTest(); + } +}; + +HEADLESS_ASYNC_DEVTOOLED_TEST_F(BrowserDomainCreateAndDeletePageTest); + +class BrowserDomainCreateAndDeleteBrowserContextTest + : public HeadlessAsyncDevTooledBrowserTest { + void RunDevTooledTest() override { + EXPECT_TRUE(embedded_test_server()->Start()); + + EXPECT_EQ(1u, GetAllWebContents(browser()).size()); + + devtools_client_->GetBrowser()->GetExperimental()->CreateBrowserContext( + browser::CreateBrowserContextParams::Builder().Build(), + base::Bind(&BrowserDomainCreateAndDeleteBrowserContextTest:: + OnCreateContextResult, + base::Unretained(this))); + } + + void OnCreateContextResult( + std::unique_ptr<browser::CreateBrowserContextResult> result) { + browser_context_id_ = result->GetBrowserContextId(); + + devtools_client_->GetBrowser()->GetExperimental()->CreateTarget( + browser::CreateTargetParams::Builder() + .SetUrl(embedded_test_server()->GetURL("/hello.html").spec()) + .SetBrowserContextId(result->GetBrowserContextId()) + .SetWidth(1) + .SetHeight(1) + .Build(), + base::Bind(&BrowserDomainCreateAndDeleteBrowserContextTest:: + OnCreateTargetResult, + base::Unretained(this))); + } + + void OnCreateTargetResult( + std::unique_ptr<browser::CreateTargetResult> result) { + EXPECT_EQ(2u, GetAllWebContents(browser()).size()); + + devtools_client_->GetBrowser()->GetExperimental()->CloseTarget( + browser::CloseTargetParams::Builder() + .SetTargetId(result->GetTargetId()) + .Build(), + base::Bind(&BrowserDomainCreateAndDeleteBrowserContextTest:: + OnCloseTargetResult, + base::Unretained(this))); + } + + void OnCloseTargetResult(std::unique_ptr<browser::CloseTargetResult> result) { + EXPECT_EQ(1u, GetAllWebContents(browser()).size()); + EXPECT_TRUE(result->GetSuccess()); + + devtools_client_->GetBrowser()->GetExperimental()->DisposeBrowserContext( + browser::DisposeBrowserContextParams::Builder() + .SetBrowserContextId(browser_context_id_) + .Build(), + base::Bind(&BrowserDomainCreateAndDeleteBrowserContextTest:: + OnDisposeBrowserContextResult, + base::Unretained(this))); + } + + void OnDisposeBrowserContextResult( + std::unique_ptr<browser::DisposeBrowserContextResult> result) { + EXPECT_TRUE(result->GetSuccess()); + FinishAsynchronousTest(); + } + + private: + std::string browser_context_id_; +}; + +HEADLESS_ASYNC_DEVTOOLED_TEST_F(BrowserDomainCreateAndDeleteBrowserContextTest); + +class BrowserDomainDisposeContextFailsIfInUse + : public HeadlessAsyncDevTooledBrowserTest { + void RunDevTooledTest() override { + EXPECT_TRUE(embedded_test_server()->Start()); + + EXPECT_EQ(1u, GetAllWebContents(browser()).size()); + devtools_client_->GetBrowser()->GetExperimental()->CreateBrowserContext( + browser::CreateBrowserContextParams::Builder().Build(), + base::Bind(&BrowserDomainDisposeContextFailsIfInUse::OnContextCreated, + base::Unretained(this))); + } + + void OnContextCreated( + std::unique_ptr<browser::CreateBrowserContextResult> result) { + context_id_ = result->GetBrowserContextId(); + + devtools_client_->GetBrowser()->GetExperimental()->CreateTarget( + browser::CreateTargetParams::Builder() + .SetUrl(embedded_test_server()->GetURL("/hello.html").spec()) + .SetBrowserContextId(context_id_) + .Build(), + base::Bind( + &BrowserDomainDisposeContextFailsIfInUse::OnCreateTargetResult, + base::Unretained(this))); + } + + void OnCreateTargetResult( + std::unique_ptr<browser::CreateTargetResult> result) { + page_id_ = result->GetTargetId(); + + devtools_client_->GetBrowser()->GetExperimental()->DisposeBrowserContext( + browser::DisposeBrowserContextParams::Builder() + .SetBrowserContextId(context_id_) + .Build(), + base::Bind(&BrowserDomainDisposeContextFailsIfInUse:: + OnDisposeBrowserContextResult, + base::Unretained(this))); + } + + void OnDisposeBrowserContextResult( + std::unique_ptr<browser::DisposeBrowserContextResult> result) { + EXPECT_FALSE(result->GetSuccess()); + + // Close the page and try again. + devtools_client_->GetBrowser()->GetExperimental()->CloseTarget( + browser::CloseTargetParams::Builder().SetTargetId(page_id_).Build(), + base::Bind( + &BrowserDomainDisposeContextFailsIfInUse::OnCloseTargetResult, + base::Unretained(this))); + } + + void OnCloseTargetResult(std::unique_ptr<browser::CloseTargetResult> result) { + EXPECT_TRUE(result->GetSuccess()); + + devtools_client_->GetBrowser()->GetExperimental()->DisposeBrowserContext( + browser::DisposeBrowserContextParams::Builder() + .SetBrowserContextId(context_id_) + .Build(), + base::Bind(&BrowserDomainDisposeContextFailsIfInUse:: + OnDisposeBrowserContextResult2, + base::Unretained(this))); + } + + void OnDisposeBrowserContextResult2( + std::unique_ptr<browser::DisposeBrowserContextResult> result) { + EXPECT_TRUE(result->GetSuccess()); + FinishAsynchronousTest(); + } + + private: + std::string context_id_; + std::string page_id_; +}; + +HEADLESS_ASYNC_DEVTOOLED_TEST_F(BrowserDomainDisposeContextFailsIfInUse); + +class BrowserDomainCreateTwoContexts : public HeadlessAsyncDevTooledBrowserTest, + public browser::ExperimentalObserver { + public: + void RunDevTooledTest() override { + EXPECT_TRUE(embedded_test_server()->Start()); + + devtools_client_->GetBrowser()->GetExperimental()->AddObserver(this); + devtools_client_->GetBrowser()->GetExperimental()->CreateBrowserContext( + browser::CreateBrowserContextParams::Builder().Build(), + base::Bind(&BrowserDomainCreateTwoContexts::OnContextOneCreated, + base::Unretained(this))); + + devtools_client_->GetBrowser()->GetExperimental()->CreateBrowserContext( + browser::CreateBrowserContextParams::Builder().Build(), + base::Bind(&BrowserDomainCreateTwoContexts::OnContextTwoCreated, + base::Unretained(this))); + } + + void OnContextOneCreated( + std::unique_ptr<browser::CreateBrowserContextResult> result) { + context_id_one_ = result->GetBrowserContextId(); + MaybeCreatePages(); + } + + void OnContextTwoCreated( + std::unique_ptr<browser::CreateBrowserContextResult> result) { + context_id_two_ = result->GetBrowserContextId(); + MaybeCreatePages(); + } + + void MaybeCreatePages() { + if (context_id_one_.empty() || context_id_two_.empty()) + return; + + devtools_client_->GetBrowser()->GetExperimental()->CreateTarget( + browser::CreateTargetParams::Builder() + .SetUrl(embedded_test_server()->GetURL("/hello.html").spec()) + .SetBrowserContextId(context_id_one_) + .Build(), + base::Bind(&BrowserDomainCreateTwoContexts::OnCreateTargetOneResult, + base::Unretained(this))); + + devtools_client_->GetBrowser()->GetExperimental()->CreateTarget( + browser::CreateTargetParams::Builder() + .SetUrl(embedded_test_server()->GetURL("/hello.html").spec()) + .SetBrowserContextId(context_id_two_) + .Build(), + base::Bind(&BrowserDomainCreateTwoContexts::OnCreateTargetTwoResult, + base::Unretained(this))); + } + + void OnCreateTargetOneResult( + std::unique_ptr<browser::CreateTargetResult> result) { + page_id_one_ = result->GetTargetId(); + MaybeTestIsolation(); + } + + void OnCreateTargetTwoResult( + std::unique_ptr<browser::CreateTargetResult> result) { + page_id_two_ = result->GetTargetId(); + MaybeTestIsolation(); + } + + void MaybeTestIsolation() { + if (page_id_one_.empty() || page_id_two_.empty()) + return; + + devtools_client_->GetBrowser()->GetExperimental()->Attach( + browser::AttachParams::Builder().SetTargetId(page_id_one_).Build(), + base::Bind(&BrowserDomainCreateTwoContexts::OnAttachedOne, + base::Unretained(this))); + + devtools_client_->GetBrowser()->GetExperimental()->Attach( + browser::AttachParams::Builder().SetTargetId(page_id_two_).Build(), + base::Bind(&BrowserDomainCreateTwoContexts::OnAttachedTwo, + base::Unretained(this))); + } + + void OnAttachedOne(std::unique_ptr<browser::AttachResult> result) { + devtools_client_->GetBrowser()->GetExperimental()->SendMessage( + browser::SendMessageParams::Builder() + .SetTargetId(page_id_one_) + .SetMessage("{\"id\":101, \"method\": \"Page.enable\"}") + .Build()); + } + + void OnAttachedTwo(std::unique_ptr<browser::AttachResult> result) { + devtools_client_->GetBrowser()->GetExperimental()->SendMessage( + browser::SendMessageParams::Builder() + .SetTargetId(page_id_two_) + .SetMessage("{\"id\":102, \"method\": \"Page.enable\"}") + .Build()); + } + + void MaybeSetCookieOnPageOne() { + if (!page_one_loaded_ || !page_two_loaded_) + return; + + devtools_client_->GetBrowser()->GetExperimental()->SendMessage( + browser::SendMessageParams::Builder() + .SetTargetId(page_id_one_) + .SetMessage("{\"id\":201, \"method\": \"Runtime.evaluate\", " + "\"params\": {\"expression\": " + "\"document.cookie = 'foo=bar';\"}}") + .Build()); + } + + void OnDispatchMessage( + const browser::DispatchMessageParams& params) override { + std::unique_ptr<base::Value> message = + base::JSONReader::Read(params.GetMessage(), base::JSON_PARSE_RFC); + const base::DictionaryValue* message_dict; + if (!message || !message->GetAsDictionary(&message_dict)) { + return; + } + std::string method; + if (message_dict->GetString("method", &method) && + method == "Page.loadEventFired") { + if (params.GetTargetId() == page_id_one_) { + page_one_loaded_ = true; + } else if (params.GetTargetId() == page_id_two_) { + page_two_loaded_ = true; + } + MaybeSetCookieOnPageOne(); + return; + } + const base::DictionaryValue* result_dict; + if (message_dict->GetDictionary("result", &result_dict)) { + // There's a nested result. We want the inner one. + if (!result_dict->GetDictionary("result", &result_dict)) + return; + std::string value; + if (params.GetTargetId() == page_id_one_) { + // TODO(alexclarke): Make some better bindings for Browser.sendMessage. + devtools_client_->GetBrowser()->GetExperimental()->SendMessage( + browser::SendMessageParams::Builder() + .SetTargetId(page_id_two_) + .SetMessage("{\"id\":202, \"method\": \"Runtime.evaluate\", " + "\"params\": {\"expression\": " + "\"document.cookie;\"}}") + .Build()); + } else if (params.GetTargetId() == page_id_two_ && + result_dict->GetString("value", &value)) { + EXPECT_EQ("", value) << "Page 2 should not share cookies from page one"; + + devtools_client_->GetBrowser()->GetExperimental()->CloseTarget( + browser::CloseTargetParams::Builder() + .SetTargetId(page_id_one_) + .Build(), + base::Bind(&BrowserDomainCreateTwoContexts::OnCloseTarget, + base::Unretained(this))); + + devtools_client_->GetBrowser()->GetExperimental()->CloseTarget( + browser::CloseTargetParams::Builder() + .SetTargetId(page_id_two_) + .Build(), + base::Bind(&BrowserDomainCreateTwoContexts::OnCloseTarget, + base::Unretained(this))); + + devtools_client_->GetBrowser()->GetExperimental()->RemoveObserver(this); + } + } + } + + void OnCloseTarget(std::unique_ptr<browser::CloseTargetResult> result) { + page_close_count_++; + + if (page_close_count_ < 2) + return; + + devtools_client_->GetBrowser()->GetExperimental()->DisposeBrowserContext( + browser::DisposeBrowserContextParams::Builder() + .SetBrowserContextId(context_id_one_) + .Build(), + base::Bind(&BrowserDomainCreateTwoContexts::OnCloseContext, + base::Unretained(this))); + + devtools_client_->GetBrowser()->GetExperimental()->DisposeBrowserContext( + browser::DisposeBrowserContextParams::Builder() + .SetBrowserContextId(context_id_two_) + .Build(), + base::Bind(&BrowserDomainCreateTwoContexts::OnCloseContext, + base::Unretained(this))); + } + + void OnCloseContext( + std::unique_ptr<browser::DisposeBrowserContextResult> result) { + EXPECT_TRUE(result->GetSuccess()); + if (++context_closed_count_ < 2) + return; + + FinishAsynchronousTest(); + } + + private: + std::string context_id_one_; + std::string context_id_two_; + std::string page_id_one_; + std::string page_id_two_; + bool page_one_loaded_ = false; + bool page_two_loaded_ = false; + int page_close_count_ = 0; + int context_closed_count_ = 0; +}; + +HEADLESS_ASYNC_DEVTOOLED_TEST_F(BrowserDomainCreateTwoContexts); + +class HeadlessDevToolsNavigationControlTest + : public HeadlessAsyncDevTooledBrowserTest, + page::ExperimentalObserver { + public: + void RunDevTooledTest() override { + EXPECT_TRUE(embedded_test_server()->Start()); + devtools_client_->GetPage()->GetExperimental()->AddObserver(this); + devtools_client_->GetPage()->Enable(); + devtools_client_->GetPage()->GetExperimental()->SetControlNavigations( + headless::page::SetControlNavigationsParams::Builder() + .SetEnabled(true) + .Build()); + devtools_client_->GetPage()->Navigate( + embedded_test_server()->GetURL("/hello.html").spec()); + } + + void OnNavigationRequested( + const headless::page::NavigationRequestedParams& params) override { + navigation_requested_ = true; + // Allow the navigation to proceed. + devtools_client_->GetPage()->GetExperimental()->ProcessNavigation( + headless::page::ProcessNavigationParams::Builder() + .SetNavigationId(params.GetNavigationId()) + .SetResponse(headless::page::NavigationResponse::PROCEED) + .Build()); + } + + void OnFrameStoppedLoading( + const page::FrameStoppedLoadingParams& params) override { + EXPECT_TRUE(navigation_requested_); + FinishAsynchronousTest(); + } + + private: + bool navigation_requested_ = false; +}; + +HEADLESS_ASYNC_DEVTOOLED_TEST_F(HeadlessDevToolsNavigationControlTest); + } // namespace headless diff --git a/chromium/headless/lib/headless_web_contents_browsertest.cc b/chromium/headless/lib/headless_web_contents_browsertest.cc index 88700d95c23..0fd928371ed 100644 --- a/chromium/headless/lib/headless_web_contents_browsertest.cc +++ b/chromium/headless/lib/headless_web_contents_browsertest.cc @@ -12,10 +12,13 @@ #include "headless/public/headless_devtools_client.h" #include "headless/public/headless_web_contents.h" #include "headless/test/headless_browser_test.h" +#include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/gfx/geometry/size.h" #include "url/gurl.h" +using testing::UnorderedElementsAre; + namespace headless { class HeadlessWebContentsTest : public HeadlessBrowserTest {}; @@ -23,34 +26,33 @@ class HeadlessWebContentsTest : public HeadlessBrowserTest {}; IN_PROC_BROWSER_TEST_F(HeadlessWebContentsTest, Navigation) { EXPECT_TRUE(embedded_test_server()->Start()); + HeadlessBrowserContext* browser_context = + browser()->CreateBrowserContextBuilder().Build(); + HeadlessWebContents* web_contents = - browser() - ->CreateWebContentsBuilder() + browser_context->CreateWebContentsBuilder() .SetInitialURL(embedded_test_server()->GetURL("/hello.html")) .Build(); EXPECT_TRUE(WaitForLoad(web_contents)); - std::vector<HeadlessWebContents*> all_web_contents = - browser()->GetAllWebContents(); - - EXPECT_EQ(static_cast<size_t>(1), all_web_contents.size()); - EXPECT_EQ(web_contents, all_web_contents[0]); + EXPECT_THAT(browser_context->GetAllWebContents(), + UnorderedElementsAre(web_contents)); } IN_PROC_BROWSER_TEST_F(HeadlessWebContentsTest, WindowOpen) { EXPECT_TRUE(embedded_test_server()->Start()); + HeadlessBrowserContext* browser_context = + browser()->CreateBrowserContextBuilder().Build(); + HeadlessWebContents* web_contents = - browser() - ->CreateWebContentsBuilder() + browser_context->CreateWebContentsBuilder() .SetInitialURL(embedded_test_server()->GetURL("/window_open.html")) .Build(); EXPECT_TRUE(WaitForLoad(web_contents)); - std::vector<HeadlessWebContents*> all_web_contents = - browser()->GetAllWebContents(); - - EXPECT_EQ(static_cast<size_t>(2), all_web_contents.size()); + EXPECT_EQ(static_cast<size_t>(2), + browser_context->GetAllWebContents().size()); } class HeadlessWebContentsScreenshotTest diff --git a/chromium/headless/lib/renderer/headless_content_renderer_client.cc b/chromium/headless/lib/renderer/headless_content_renderer_client.cc index 5347d609222..82faf2165a5 100644 --- a/chromium/headless/lib/renderer/headless_content_renderer_client.cc +++ b/chromium/headless/lib/renderer/headless_content_renderer_client.cc @@ -4,6 +4,9 @@ #include "headless/lib/renderer/headless_content_renderer_client.h" +#include "base/strings/utf_string_conversions.h" +#include "content/public/renderer/render_frame.h" + namespace headless { HeadlessContentRendererClient::HeadlessContentRendererClient() {} diff --git a/chromium/headless/lib/resources/devtools_discovery_page.html b/chromium/headless/lib/resources/devtools_discovery_page.html index 47534f7c462..86809b9d1c5 100644 --- a/chromium/headless/lib/resources/devtools_discovery_page.html +++ b/chromium/headless/lib/resources/devtools_discovery_page.html @@ -5,49 +5,49 @@ </style> <script> -function onLoad() { - var tabs_list_request = new XMLHttpRequest(); - tabs_list_request.open("GET", "/json/list?t=" + new Date().getTime(), true); - tabs_list_request.onreadystatechange = onReady; - tabs_list_request.send(); +const fetchjson = (url) => fetch(url).then(r => r.json()); + +function loadData() { + const getList = fetchjson("/json/list"); + const getVersion = fetchjson('/json/version'); + Promise.all([getList, getVersion]).then(parseResults); } -function onReady() { - if(this.readyState == 4 && this.status == 200) { - if(this.response != null) - var responseJSON = JSON.parse(this.response); - for (var i = 0; i < responseJSON.length; ++i) - appendItem(responseJSON[i]); - } +function parseResults([listData, versionData]){ + const version = versionData['WebKit-Version']; + const hash = version.match(/\s\(@(\b[0-9a-f]{5,40}\b)/)[1]; + listData.forEach(item => appendItem(item, hash)); } -function appendItem(item_object) { - var frontend_ref; - if (item_object.devtoolsFrontendUrl) { - frontend_ref = document.createElement("a"); - frontend_ref.href = item_object.devtoolsFrontendUrl; - frontend_ref.title = item_object.title; +function appendItem(item, hash) { + let link; + if (item.devtoolsFrontendUrl) { + link = document.createElement("a"); + var devtoolsFrontendUrl = item.devtoolsFrontendUrl.replace(/^\/devtools\//,''); + link.href = `https://chrome-devtools-frontend.appspot.com/serve_file/@${hash}/${devtoolsFrontendUrl}&remoteFrontend=true`; + link.title = item.title; } else { - frontend_ref = document.createElement("div"); - frontend_ref.title = "The tab already has active debugging session"; + link = document.createElement("div"); + link.title = "The tab already has active debugging session"; } var text = document.createElement("div"); - if (item_object.title) - text.innerText = item_object.title; + if (item.title) + text.textContent = item.title; else - text.innerText = "(untitled tab)"; - text.style.cssText = "background-image:url(" + item_object.faviconUrl + ")"; - frontend_ref.appendChild(text); + text.textContent = "(untitled tab)"; + if (item.faviconUrl) + text.style.cssText = "background-image:url(" + item.faviconUrl + ")"; + link.appendChild(text); - var item = document.createElement("p"); - item.appendChild(frontend_ref); + var p = document.createElement("p"); + p.appendChild(link); - document.getElementById("items").appendChild(item); + document.getElementById("items").appendChild(p); } </script> </head> -<body onload='onLoad()'> +<body onload='loadData()'> <div id='caption'>Inspectable WebContents</div> <div id='items'></div> </body> diff --git a/chromium/headless/lib/resources/headless_lib_resources.grd b/chromium/headless/lib/resources/headless_lib_resources.grd index 060ab47b9b2..3137ec64834 100644 --- a/chromium/headless/lib/resources/headless_lib_resources.grd +++ b/chromium/headless/lib/resources/headless_lib_resources.grd @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> - -<grit latest_public_release="0" current_release="1"> +<grit latest_public_release="0" current_release="1" output_all_resource_defines="false"> <outputs> <output filename="grit/headless_lib_resources.h" type="rc_header"> <emit emit_type='prepend'></emit> diff --git a/chromium/headless/public/domains/types_unittest.cc b/chromium/headless/public/domains/types_unittest.cc index c665d95b225..bf59b01151a 100644 --- a/chromium/headless/public/domains/types_unittest.cc +++ b/chromium/headless/public/domains/types_unittest.cc @@ -9,23 +9,23 @@ namespace headless { TEST(TypesTest, IntegerProperty) { - std::unique_ptr<accessibility::GetAXNodeParams> object( - accessibility::GetAXNodeParams::Builder().SetNodeId(123).Build()); + std::unique_ptr<page::NavigateToHistoryEntryParams> object( + page::NavigateToHistoryEntryParams::Builder().SetEntryId(123).Build()); EXPECT_TRUE(object); - EXPECT_EQ(123, object->GetNodeId()); + EXPECT_EQ(123, object->GetEntryId()); - std::unique_ptr<accessibility::GetAXNodeParams> clone(object->Clone()); + std::unique_ptr<page::NavigateToHistoryEntryParams> clone(object->Clone()); EXPECT_TRUE(clone); - EXPECT_EQ(123, clone->GetNodeId()); + EXPECT_EQ(123, clone->GetEntryId()); } TEST(TypesTest, IntegerPropertyParseError) { - const char* json = "{\"nodeId\": \"foo\"}"; + const char* json = "{\"entryId\": \"foo\"}"; std::unique_ptr<base::Value> object = base::JSONReader::Read(json); EXPECT_TRUE(object); ErrorReporter errors; - EXPECT_FALSE(accessibility::GetAXNodeParams::Parse(*object, &errors)); + EXPECT_FALSE(page::NavigateToHistoryEntryParams::Parse(*object, &errors)); EXPECT_TRUE(errors.HasErrors()); } diff --git a/chromium/headless/public/headless_browser.cc b/chromium/headless/public/headless_browser.cc index b1e5dbcc026..cbfbc5e400e 100644 --- a/chromium/headless/public/headless_browser.cc +++ b/chromium/headless/public/headless_browser.cc @@ -2,9 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "content/public/common/user_agent.h" #include "headless/public/headless_browser.h" +#include <utility> + +#include "content/public/common/user_agent.h" + using Options = headless::HeadlessBrowser::Options; using Builder = headless::HeadlessBrowser::Options::Builder; @@ -13,14 +16,19 @@ namespace headless { // Product name for building the default user agent string. namespace { const char kProductName[] = "HeadlessChrome"; +constexpr gfx::Size kDefaultWindowSize(800, 600); } Options::Options(int argc, const char** argv) : argc(argc), argv(argv), - user_agent(content::BuildUserAgentFromProduct(kProductName)), message_pump(nullptr), - single_process_mode(false) {} + single_process_mode(false), + disable_sandbox(false), + gl_implementation("osmesa"), + user_agent(content::BuildUserAgentFromProduct(kProductName)), + window_size(kDefaultWindowSize), + incognito_mode(true) {} Options::Options(Options&& options) = default; @@ -64,8 +72,28 @@ Builder& Builder::SetSingleProcessMode(bool single_process_mode) { return *this; } -Builder& Builder::SetProtocolHandlers(ProtocolHandlerMap protocol_handlers) { - options_.protocol_handlers = std::move(protocol_handlers); +Builder& Builder::SetDisableSandbox(bool disable_sandbox) { + options_.disable_sandbox = disable_sandbox; + return *this; +} + +Builder& Builder::SetGLImplementation(const std::string& gl_implementation) { + options_.gl_implementation = gl_implementation; + return *this; +} + +Builder& Builder::SetUserDataDir(const base::FilePath& user_data_dir) { + options_.user_data_dir = user_data_dir; + return *this; +} + +Builder& Builder::SetWindowSize(const gfx::Size& window_size) { + options_.window_size = window_size; + return *this; +} + +Builder& Builder::SetIncognitoMode(bool incognito_mode) { + options_.incognito_mode = incognito_mode; return *this; } diff --git a/chromium/headless/public/headless_browser.h b/chromium/headless/public/headless_browser.h index 2e9145153ae..954b79e0db4 100644 --- a/chromium/headless/public/headless_browser.h +++ b/chromium/headless/public/headless_browser.h @@ -11,6 +11,7 @@ #include <vector> #include "base/callback.h" +#include "base/files/file_path.h" #include "base/macros.h" #include "base/memory/ref_counted.h" #include "headless/public/headless_browser_context.h" @@ -18,16 +19,13 @@ #include "headless/public/headless_web_contents.h" #include "net/base/host_port_pair.h" #include "net/base/ip_endpoint.h" +#include "ui/gfx/geometry/size.h" namespace base { class MessagePump; class SingleThreadTaskRunner; } -namespace gfx { -class Size; -} - namespace headless { // This class represents the global headless browser instance. To get a pointer @@ -38,32 +36,43 @@ class HEADLESS_EXPORT HeadlessBrowser { public: struct Options; - // Open a new tab. Returns a builder object which can be used to set - // properties for the new tab. - virtual HeadlessWebContents::Builder CreateWebContentsBuilder() = 0; + // Create a new browser context which can be used to create tabs and isolate + // them from one another. + // Pointer to HeadlessBrowserContext becomes invalid after: + // a) Calling HeadlessBrowserContext::Close or + // b) Calling HeadlessBrowser::Shutdown + virtual HeadlessBrowserContext::Builder CreateBrowserContextBuilder() = 0; - // Deprecated. Use CreateWebContentsBuilder() instead. - virtual HeadlessWebContents* CreateWebContents(const GURL& initial_url, - const gfx::Size& size) = 0; + virtual std::vector<HeadlessBrowserContext*> GetAllBrowserContexts() = 0; - virtual std::vector<HeadlessWebContents*> GetAllWebContents() = 0; + // Returns the HeadlessWebContents associated with the + // |devtools_agent_host_id| if any. Otherwise returns null. + virtual HeadlessWebContents* GetWebContentsForDevToolsAgentHostId( + const std::string& devtools_agent_host_id) = 0; - // Returns a task runner for submitting work to the browser main thread. - virtual scoped_refptr<base::SingleThreadTaskRunner> BrowserMainThread() - const = 0; + // Returns HeadlessBrowserContext associated with the given id if any. + // Otherwise returns null. + virtual HeadlessBrowserContext* GetBrowserContextForId( + const std::string& id) = 0; // Returns a task runner for submitting work to the browser file thread. virtual scoped_refptr<base::SingleThreadTaskRunner> BrowserFileThread() const = 0; + // Returns a task runner for submitting work to the browser io thread. + virtual scoped_refptr<base::SingleThreadTaskRunner> BrowserIOThread() + const = 0; + + // Returns a task runner for submitting work to the browser main thread. + virtual scoped_refptr<base::SingleThreadTaskRunner> BrowserMainThread() + const = 0; + // Requests browser to stop as soon as possible. |Run| will return as soon as // browser stops. + // IMPORTANT: All pointers to HeadlessBrowserContexts and HeadlessWebContents + // become invalid after calling this function. virtual void Shutdown() = 0; - // Create a new browser context, which can be used to isolate - // HeadlessWebContents from one another. - virtual HeadlessBrowserContext::Builder CreateBrowserContextBuilder() = 0; - protected: HeadlessBrowser() {} virtual ~HeadlessBrowser() {} @@ -85,33 +94,52 @@ struct HeadlessBrowser::Options { int argc; const char** argv; - std::string user_agent; - std::string navigator_platform; - // Address at which DevTools should listen for connections. Disabled by // default. net::IPEndPoint devtools_endpoint; + // Optional message pump that overrides the default. Must outlive the browser. + base::MessagePump* message_pump; + + // Run the browser in single process mode instead of using separate renderer + // processes as per default. Note that this also disables any sandboxing of + // web content, which can be a security risk. + bool single_process_mode; + + // Run the browser without renderer sandbox. This option can be + // a security risk and should be used with caution. + bool disable_sandbox; + + // Choose the GL implementation to use for rendering. A suitable + // implementantion is selected by default. Setting this to an empty + // string can be used to disable GL rendering (e.g., WebGL support). + std::string gl_implementation; + + // Default per-context options, can be specialized on per-context basis. + + std::string user_agent; + // Address of the HTTP/HTTPS proxy server to use. The system proxy settings // are used by default. net::HostPortPair proxy_server; - // Optional message pump that overrides the default. Must outlive the browser. - base::MessagePump* message_pump; - // Comma-separated list of rules that control how hostnames are mapped. See // chrome::switches::kHostRules for a description for the format. std::string host_resolver_rules; - // Run the browser in single process mode instead of using separate renderer - // processes as per default. Note that this also disables any sandboxing of - // web content, which can be a security risk. - bool single_process_mode; + // Default window size. This is also used to create the window tree host and + // as initial screen size. Defaults to 800x600. + gfx::Size window_size; - // Custom network protocol handlers. These can be used to override URL - // fetching for different network schemes. - ProtocolHandlerMap protocol_handlers; + // Path to user data directory, where browser will look for its state. + // If empty, default directory (where the binary is located) will be used. + base::FilePath user_data_dir; + // Run a browser context in an incognito mode. Enabled by default. + bool incognito_mode; + + // Reminder: when adding a new field here, do not forget to add it to + // HeadlessBrowserContextOptions (where appropriate). private: Options(int argc, const char** argv); @@ -124,13 +152,22 @@ class HeadlessBrowser::Options::Builder { Builder(); ~Builder(); - Builder& SetUserAgent(const std::string& user_agent); + // Browser-wide settings. + Builder& EnableDevToolsServer(const net::IPEndPoint& endpoint); Builder& SetMessagePump(base::MessagePump* message_pump); + Builder& SetSingleProcessMode(bool single_process_mode); + Builder& SetDisableSandbox(bool disable_sandbox); + Builder& SetGLImplementation(const std::string& gl_implementation); + + // Per-context settings. + + Builder& SetUserAgent(const std::string& user_agent); Builder& SetProxyServer(const net::HostPortPair& proxy_server); Builder& SetHostResolverRules(const std::string& host_resolver_rules); - Builder& SetSingleProcessMode(bool single_process_mode); - Builder& SetProtocolHandlers(ProtocolHandlerMap protocol_handlers); + Builder& SetWindowSize(const gfx::Size& window_size); + Builder& SetUserDataDir(const base::FilePath& user_data_dir); + Builder& SetIncognitoMode(bool incognito_mode); Options Build(); diff --git a/chromium/headless/public/headless_browser_context.h b/chromium/headless/public/headless_browser_context.h index f8c581c2fd5..b779f6a38da 100644 --- a/chromium/headless/public/headless_browser_context.h +++ b/chromium/headless/public/headless_browser_context.h @@ -5,11 +5,31 @@ #ifndef HEADLESS_PUBLIC_HEADLESS_BROWSER_CONTEXT_H_ #define HEADLESS_PUBLIC_HEADLESS_BROWSER_CONTEXT_H_ +#include <list> +#include <memory> +#include <string> +#include <unordered_map> +#include <vector> + +#include "base/callback.h" +#include "base/optional.h" +#include "content/public/common/web_preferences.h" #include "headless/public/headless_export.h" +#include "headless/public/headless_web_contents.h" +#include "net/base/host_port_pair.h" #include "net/url_request/url_request_job_factory.h" +namespace base { +class FilePath; +} + namespace headless { class HeadlessBrowserImpl; +class HeadlessBrowserContextOptions; + +// Imported into headless namespace for +// Builder::SetOverrideWebPreferencesCallback(). +using content::WebPreferences; using ProtocolHandlerMap = std::unordered_map< std::string, @@ -17,12 +37,34 @@ using ProtocolHandlerMap = std::unordered_map< // Represents an isolated session with a unique cache, cookies, and other // profile/session related data. +// When browser context is deleted, all associated web contents are closed. class HEADLESS_EXPORT HeadlessBrowserContext { public: class Builder; virtual ~HeadlessBrowserContext() {} + // Open a new tab. Returns a builder object which can be used to set + // properties for the new tab. + // Pointer to HeadlessWebContents becomes invalid after: + // a) Calling HeadlessWebContents::Close, or + // b) Calling HeadlessBrowserContext::Close on associated browser context, or + // c) Calling HeadlessBrowser::Shutdown. + virtual HeadlessWebContents::Builder CreateWebContentsBuilder() = 0; + + // Returns all web contents owned by this browser context. + virtual std::vector<HeadlessWebContents*> GetAllWebContents() = 0; + + // See HeadlessBrowser::GetWebContentsForDevToolsAgentHostId. + virtual HeadlessWebContents* GetWebContentsForDevToolsAgentHostId( + const std::string& devtools_agent_host_id) = 0; + + // Destroy this BrowserContext and all WebContents associated with it. + virtual void Close() = 0; + + // GUID for this browser context. + virtual const std::string& Id() const = 0; + // TODO(skyostil): Allow saving and restoring contexts (crbug.com/617931). protected: @@ -41,15 +83,70 @@ class HEADLESS_EXPORT HeadlessBrowserContext::Builder { // fetching for different network schemes. Builder& SetProtocolHandlers(ProtocolHandlerMap protocol_handlers); - std::unique_ptr<HeadlessBrowserContext> Build(); + // Specify JS mojo module bindings to be installed, one per mojom file. + // Note a single mojom file could potentially define many interfaces. + // |mojom_name| the name including path of the .mojom file. + // |js_bindings| compiletime generated javascript bindings. Typically loaded + // from gen/path/name.mojom.js. + Builder& AddJsMojoBindings(const std::string& mojom_name, + const std::string& js_bindings); + + // By default if you add mojo bindings, http and https are disabled because + // its almost certinly unsafe for arbitary sites on the internet to have + // access to these bindings. If you know what you're doing it may be OK to + // turn them back on. E.g. if headless_lib is being used in a testing + // framework which serves the web content from disk that's likely ok. + // + // That said, best pratice is to add a ProtocolHandler to serve the + // webcontent over a custom protocol. That way you can be sure that only the + // things you intend have access to mojo. + Builder& EnableUnsafeNetworkAccessWithMojoBindings( + bool enable_http_and_https_if_mojo_used); + + // Set a callback that is invoked to override WebPreferences for RenderViews + // created within this HeadlessBrowserContext. Called whenever the + // WebPreferences of a RenderView change. Executed on the browser main thread. + // + // WARNING: We cannot provide any guarantees about the stability of the + // exposed WebPreferences API, so use with care. + Builder& SetOverrideWebPreferencesCallback( + base::Callback<void(WebPreferences*)> callback); + + // By default |HeadlessBrowserContext| inherits the following options from + // the browser instance. The methods below can be used to override these + // settings. See HeadlessBrowser::Options for their meaning. + Builder& SetUserAgent(const std::string& user_agent); + Builder& SetProxyServer(const net::HostPortPair& proxy_server); + Builder& SetHostResolverRules(const std::string& host_resolver_rules); + Builder& SetWindowSize(const gfx::Size& window_size); + Builder& SetUserDataDir(const base::FilePath& user_data_dir); + Builder& SetIncognitoMode(bool incognito_mode); + + HeadlessBrowserContext* Build(); private: friend class HeadlessBrowserImpl; + friend class HeadlessBrowserContextImpl; explicit Builder(HeadlessBrowserImpl* browser); + struct MojoBindings { + MojoBindings(); + MojoBindings(const std::string& mojom_name, const std::string& js_bindings); + ~MojoBindings(); + + std::string mojom_name; + std::string js_bindings; + + private: + DISALLOW_COPY_AND_ASSIGN(MojoBindings); + }; + HeadlessBrowserImpl* browser_; - ProtocolHandlerMap protocol_handlers_; + std::unique_ptr<HeadlessBrowserContextOptions> options_; + + std::list<MojoBindings> mojo_bindings_; + bool enable_http_and_https_if_mojo_used_; DISALLOW_COPY_AND_ASSIGN(Builder); }; diff --git a/chromium/headless/public/headless_devtools_client.h b/chromium/headless/public/headless_devtools_client.h index a71ce657200..36e6ea3877a 100644 --- a/chromium/headless/public/headless_devtools_client.h +++ b/chromium/headless/public/headless_devtools_client.h @@ -21,6 +21,9 @@ class Domain; namespace application_cache { class Domain; } +namespace browser { +class Domain; +} namespace cache_storage { class Domain; } @@ -69,6 +72,9 @@ class Domain; namespace layer_tree { class Domain; } +namespace log { +class Domain; +} namespace memory { class Domain; } @@ -115,6 +121,7 @@ class HEADLESS_EXPORT HeadlessDevToolsClient { virtual accessibility::Domain* GetAccessibility() = 0; virtual animation::Domain* GetAnimation() = 0; virtual application_cache::Domain* GetApplicationCache() = 0; + virtual browser::Domain* GetBrowser() = 0; virtual cache_storage::Domain* GetCacheStorage() = 0; virtual console::Domain* GetConsole() = 0; virtual css::Domain* GetCSS() = 0; @@ -131,6 +138,7 @@ class HEADLESS_EXPORT HeadlessDevToolsClient { virtual inspector::Domain* GetInspector() = 0; virtual io::Domain* GetIO() = 0; virtual layer_tree::Domain* GetLayerTree() = 0; + virtual log::Domain* GetLog() = 0; virtual memory::Domain* GetMemory() = 0; virtual network::Domain* GetNetwork() = 0; virtual page::Domain* GetPage() = 0; diff --git a/chromium/headless/public/headless_web_contents.h b/chromium/headless/public/headless_web_contents.h index a193d5042e6..0ca6a588bb5 100644 --- a/chromium/headless/public/headless_web_contents.h +++ b/chromium/headless/public/headless_web_contents.h @@ -5,14 +5,19 @@ #ifndef HEADLESS_PUBLIC_HEADLESS_WEB_CONTENTS_H_ #define HEADLESS_PUBLIC_HEADLESS_WEB_CONTENTS_H_ +#include <list> +#include <string> +#include <utility> + #include "base/callback.h" #include "base/macros.h" #include "headless/public/headless_export.h" +#include "mojo/public/cpp/bindings/interface_request.h" #include "ui/gfx/geometry/size.h" #include "url/gurl.h" namespace headless { -class HeadlessBrowserContext; +class HeadlessBrowserContextImpl; class HeadlessBrowserImpl; class HeadlessDevToolsTarget; @@ -73,14 +78,24 @@ class HEADLESS_EXPORT HeadlessWebContents::Builder { // about:blank. Builder& SetInitialURL(const GURL& initial_url); - // Specify the initial window size (default is 800x600). + // Specify the initial window size (default is configured in browser options). Builder& SetWindowSize(const gfx::Size& size); - // Set a browser context for storing session data (e.g., cookies, cache, local - // storage) for the tab. Several tabs can share the same browser context. If - // unset, the default browser context will be used. The browser context must - // outlive this HeadlessWebContents. - Builder& SetBrowserContext(HeadlessBrowserContext* browser_context); + // Specify an embedder provided Mojo service to be installed. The + // |service_factory| callback is called on demand by Mojo to instantiate the + // service if a client asks for it. + template <typename Interface> + Builder& AddMojoService( + const base::Callback<void(mojo::InterfaceRequest<Interface>)>& + service_factory) { + return AddMojoService( + Interface::Name_, + base::Bind(&Builder::ForwardToServiceFactory<Interface>, + service_factory)); + } + Builder& AddMojoService(const std::string& service_name, + const base::Callback<void( + mojo::ScopedMessagePipeHandle)>& service_factory); // The returned object is owned by HeadlessBrowser. Call // HeadlessWebContents::Close() to dispose it. @@ -88,14 +103,38 @@ class HEADLESS_EXPORT HeadlessWebContents::Builder { private: friend class HeadlessBrowserImpl; + friend class HeadlessBrowserContextImpl; friend class HeadlessWebContentsImpl; - explicit Builder(HeadlessBrowserImpl* browser); + explicit Builder(HeadlessBrowserContextImpl* browser_context); + + template <typename Interface> + static void ForwardToServiceFactory( + const base::Callback<void(mojo::InterfaceRequest<Interface>)>& + service_factory, + mojo::ScopedMessagePipeHandle handle) { + service_factory.Run(mojo::MakeRequest<Interface>(std::move(handle))); + } + + struct MojoService { + MojoService(); + MojoService(const std::string& service_name, + const base::Callback<void(mojo::ScopedMessagePipeHandle)>& + service_factory); + ~MojoService(); + + std::string service_name; + base::Callback<void(mojo::ScopedMessagePipeHandle)> service_factory; + + private: + DISALLOW_COPY_AND_ASSIGN(MojoService); + }; + + HeadlessBrowserContextImpl* browser_context_; - HeadlessBrowserImpl* browser_; GURL initial_url_ = GURL("about:blank"); - gfx::Size window_size_ = gfx::Size(800, 600); - HeadlessBrowserContext* browser_context_; + gfx::Size window_size_; + std::list<MojoService> mojo_services_; DISALLOW_COPY_AND_ASSIGN(Builder); }; diff --git a/chromium/headless/public/internal/value_conversions.h b/chromium/headless/public/internal/value_conversions.h index 17c6eda3f22..c25a0cd1ce2 100644 --- a/chromium/headless/public/internal/value_conversions.h +++ b/chromium/headless/public/internal/value_conversions.h @@ -32,22 +32,22 @@ struct FromValue { // partially specialize vector types. template <typename T> std::unique_ptr<base::Value> ToValueImpl(int value, T*) { - return base::WrapUnique(new base::FundamentalValue(value)); + return base::MakeUnique<base::FundamentalValue>(value); } template <typename T> std::unique_ptr<base::Value> ToValueImpl(double value, T*) { - return base::WrapUnique(new base::FundamentalValue(value)); + return base::MakeUnique<base::FundamentalValue>(value); } template <typename T> std::unique_ptr<base::Value> ToValueImpl(bool value, T*) { - return base::WrapUnique(new base::FundamentalValue(value)); + return base::MakeUnique<base::FundamentalValue>(value); } template <typename T> std::unique_ptr<base::Value> ToValueImpl(const std::string& value, T*) { - return base::WrapUnique(new base::StringValue(value)); + return base::MakeUnique<base::StringValue>(value); } template <typename T> diff --git a/chromium/headless/public/util/black_hole_protocol_handler.cc b/chromium/headless/public/util/black_hole_protocol_handler.cc new file mode 100644 index 00000000000..3f036c59706 --- /dev/null +++ b/chromium/headless/public/util/black_hole_protocol_handler.cc @@ -0,0 +1,66 @@ +// 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 "headless/public/util/black_hole_protocol_handler.h" + +#include "base/threading/thread_task_runner_handle.h" +#include "net/base/io_buffer.h" +#include "net/base/net_errors.h" +#include "net/url_request/url_request_job.h" + +namespace headless { +namespace { +class BlackHoleRequestJob : public net::URLRequestJob { + public: + BlackHoleRequestJob(net::URLRequest* request, + net::NetworkDelegate* network_delegate); + + // net::URLRequestJob implementation: + void Start() override; + void Kill() override; + + private: + ~BlackHoleRequestJob() override; + + void StartAsync(); + + base::WeakPtrFactory<BlackHoleRequestJob> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(BlackHoleRequestJob); +}; + +BlackHoleRequestJob::BlackHoleRequestJob(net::URLRequest* request, + net::NetworkDelegate* network_delegate) + : net::URLRequestJob(request, network_delegate), weak_factory_(this) {} + +BlackHoleRequestJob::~BlackHoleRequestJob() {} + +void BlackHoleRequestJob::Start() { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(&BlackHoleRequestJob::StartAsync, weak_factory_.GetWeakPtr())); +} + +void BlackHoleRequestJob::StartAsync() { + // Fail every request! + NotifyStartError(net::URLRequestStatus(net::URLRequestStatus::FAILED, + net::ERR_FILE_NOT_FOUND)); +} + +void BlackHoleRequestJob::Kill() { + weak_factory_.InvalidateWeakPtrs(); + URLRequestJob::Kill(); +} +} // namespace + +BlackHoleProtocolHandler::BlackHoleProtocolHandler() {} +BlackHoleProtocolHandler::~BlackHoleProtocolHandler() {} + +net::URLRequestJob* BlackHoleProtocolHandler::MaybeCreateJob( + net::URLRequest* request, + net::NetworkDelegate* network_delegate) const { + return new BlackHoleRequestJob(request, network_delegate); +} + +} // namespace headless diff --git a/chromium/headless/public/util/black_hole_protocol_handler.h b/chromium/headless/public/util/black_hole_protocol_handler.h new file mode 100644 index 00000000000..7d728904837 --- /dev/null +++ b/chromium/headless/public/util/black_hole_protocol_handler.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 HEADLESS_PUBLIC_UTIL_BLACK_HOLE_PROTOCOL_HANDLER_H_ +#define HEADLESS_PUBLIC_UTIL_BLACK_HOLE_PROTOCOL_HANDLER_H_ + +#include <map> + +#include "net/url_request/url_request_job_factory.h" + +namespace headless { + +// A protocol handler that fails any request sent to it. +class BlackHoleProtocolHandler + : public net::URLRequestJobFactory::ProtocolHandler { + public: + BlackHoleProtocolHandler(); + ~BlackHoleProtocolHandler() override; + + net::URLRequestJob* MaybeCreateJob( + net::URLRequest* request, + net::NetworkDelegate* network_delegate) const override; + + private: + DISALLOW_COPY_AND_ASSIGN(BlackHoleProtocolHandler); +}; + +} // namespace headless + +#endif // HEADLESS_PUBLIC_UTIL_BLACK_HOLE_PROTOCOL_HANDLER_H_ diff --git a/chromium/headless/public/util/deterministic_dispatcher.cc b/chromium/headless/public/util/deterministic_dispatcher.cc new file mode 100644 index 00000000000..70696eaeaa9 --- /dev/null +++ b/chromium/headless/public/util/deterministic_dispatcher.cc @@ -0,0 +1,95 @@ +// 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 "headless/public/util/deterministic_dispatcher.h" + +#include <utility> + +#include "base/bind.h" +#include "base/logging.h" +#include "headless/public/util/managed_dispatch_url_request_job.h" + +namespace headless { + +DeterministicDispatcher::DeterministicDispatcher( + scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner) + : io_thread_task_runner_(std::move(io_thread_task_runner)), + dispatch_pending_(false) {} + +DeterministicDispatcher::~DeterministicDispatcher() {} + +void DeterministicDispatcher::JobCreated(ManagedDispatchURLRequestJob* job) { + base::AutoLock lock(lock_); + pending_requests_.push_back(job); +} + +void DeterministicDispatcher::JobKilled(ManagedDispatchURLRequestJob* job) { + base::AutoLock lock(lock_); + for (auto it = pending_requests_.begin(); it != pending_requests_.end(); + it++) { + if (*it == job) { + pending_requests_.erase(it); + break; + } + } + ready_status_map_.erase(job); + // We rely on JobDeleted getting called to call MaybeDispatchJobLocked. +} + +void DeterministicDispatcher::JobFailed(ManagedDispatchURLRequestJob* job, + net::Error error) { + base::AutoLock lock(lock_); + ready_status_map_[job] = error; + MaybeDispatchJobLocked(); +} + +void DeterministicDispatcher::DataReady(ManagedDispatchURLRequestJob* job) { + base::AutoLock lock(lock_); + ready_status_map_[job] = net::OK; + MaybeDispatchJobLocked(); +} + +void DeterministicDispatcher::JobDeleted(ManagedDispatchURLRequestJob* job) { + base::AutoLock lock(lock_); + MaybeDispatchJobLocked(); +} + +void DeterministicDispatcher::MaybeDispatchJobLocked() { + if (dispatch_pending_ || ready_status_map_.empty()) + return; + + dispatch_pending_ = true; + io_thread_task_runner_->PostTask( + FROM_HERE, + base::Bind(&DeterministicDispatcher::MaybeDispatchJobOnIOThreadTask, + base::Unretained(this))); +} + +void DeterministicDispatcher::MaybeDispatchJobOnIOThreadTask() { + ManagedDispatchURLRequestJob* job; + net::Error job_status; + + { + base::AutoLock lock(lock_); + CHECK(!pending_requests_.empty()); + dispatch_pending_ = false; + job = pending_requests_.front(); + StatusMap::const_iterator it = ready_status_map_.find(job); + // Bail out if the oldest job is not be ready for dispatch yet. + if (it == ready_status_map_.end()) + return; + + job_status = it->second; + ready_status_map_.erase(it); + pending_requests_.pop_front(); + } + + if (job_status == net::OK) { + job->OnHeadersComplete(); + } else { + job->OnStartError(job_status); + } +} + +} // namespace headless diff --git a/chromium/headless/public/util/deterministic_dispatcher.h b/chromium/headless/public/util/deterministic_dispatcher.h new file mode 100644 index 00000000000..c2e73b61710 --- /dev/null +++ b/chromium/headless/public/util/deterministic_dispatcher.h @@ -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. + +#ifndef HEADLESS_PUBLIC_UTIL_DETERMINISTIC_DISPATCHER_H_ +#define HEADLESS_PUBLIC_UTIL_DETERMINISTIC_DISPATCHER_H_ + +#include <deque> +#include <map> + +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/single_thread_task_runner.h" +#include "base/synchronization/lock.h" +#include "headless/public/util/url_request_dispatcher.h" +#include "net/base/net_errors.h" + +namespace headless { + +class ManagedDispatchURLRequestJob; + +// The purpose of this class is to queue up calls to OnHeadersComplete and +// OnStartError and dispatch them in order of URLRequestJob creation. This +// helps make renders deterministic at the cost of slower page loads. +class DeterministicDispatcher : public URLRequestDispatcher { + public: + explicit DeterministicDispatcher( + scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner); + + ~DeterministicDispatcher() override; + + // UrlRequestDispatcher implementation: + void JobCreated(ManagedDispatchURLRequestJob* job) override; + void JobKilled(ManagedDispatchURLRequestJob* job) override; + void JobFailed(ManagedDispatchURLRequestJob* job, net::Error error) override; + void DataReady(ManagedDispatchURLRequestJob* job) override; + void JobDeleted(ManagedDispatchURLRequestJob* job) override; + + private: + void MaybeDispatchJobLocked(); + void MaybeDispatchJobOnIOThreadTask(); + + scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner_; + + // Protects all members below. + base::Lock lock_; + + std::deque<ManagedDispatchURLRequestJob*> pending_requests_; + + using StatusMap = std::map<ManagedDispatchURLRequestJob*, net::Error>; + StatusMap ready_status_map_; + + // Whether or not a MaybeDispatchJobOnIoThreadTask has been posted on the + // |io_thread_task_runner_| + bool dispatch_pending_; + + DISALLOW_COPY_AND_ASSIGN(DeterministicDispatcher); +}; + +} // namespace headless + +#endif // HEADLESS_PUBLIC_UTIL_DETERMINISTIC_DISPATCHER_H_ diff --git a/chromium/headless/public/util/deterministic_dispatcher_test.cc b/chromium/headless/public/util/deterministic_dispatcher_test.cc new file mode 100644 index 00000000000..b4c1c1fe089 --- /dev/null +++ b/chromium/headless/public/util/deterministic_dispatcher_test.cc @@ -0,0 +1,113 @@ +// 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 "headless/public/util/deterministic_dispatcher.h" + +#include <memory> +#include <string> +#include <vector> + +#include "base/run_loop.h" +#include "base/single_thread_task_runner.h" +#include "headless/public/util/testing/fake_managed_dispatch_url_request_job.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::ElementsAre; + +namespace headless { + +class DeterministicDispatcherTest : public ::testing::Test { + protected: + DeterministicDispatcherTest() {} + ~DeterministicDispatcherTest() override {} + + void SetUp() override { + deterministic_dispatcher_.reset( + new DeterministicDispatcher(loop_.task_runner())); + } + + base::MessageLoop loop_; + std::unique_ptr<DeterministicDispatcher> deterministic_dispatcher_; +}; + +TEST_F(DeterministicDispatcherTest, DispatchDataReadyInReverseOrder) { + std::vector<std::string> notifications; + std::unique_ptr<FakeManagedDispatchURLRequestJob> job1( + new FakeManagedDispatchURLRequestJob(deterministic_dispatcher_.get(), 1, + ¬ifications)); + std::unique_ptr<FakeManagedDispatchURLRequestJob> job2( + new FakeManagedDispatchURLRequestJob(deterministic_dispatcher_.get(), 2, + ¬ifications)); + std::unique_ptr<FakeManagedDispatchURLRequestJob> job3( + new FakeManagedDispatchURLRequestJob(deterministic_dispatcher_.get(), 3, + ¬ifications)); + std::unique_ptr<FakeManagedDispatchURLRequestJob> job4( + new FakeManagedDispatchURLRequestJob(deterministic_dispatcher_.get(), 4, + ¬ifications)); + job4->DispatchHeadersComplete(); + job3->DispatchHeadersComplete(); + job2->DispatchHeadersComplete(); + job1->DispatchHeadersComplete(); + + EXPECT_TRUE(notifications.empty()); + + base::RunLoop().RunUntilIdle(); + EXPECT_THAT(notifications, ElementsAre("id: 1 OK")); + + job1.reset(); + base::RunLoop().RunUntilIdle(); + EXPECT_THAT(notifications, ElementsAre("id: 1 OK", "id: 2 OK")); + + job2.reset(); + base::RunLoop().RunUntilIdle(); + EXPECT_THAT(notifications, ElementsAre("id: 1 OK", "id: 2 OK", "id: 3 OK")); + + job3.reset(); + base::RunLoop().RunUntilIdle(); + EXPECT_THAT(notifications, + ElementsAre("id: 1 OK", "id: 2 OK", "id: 3 OK", "id: 4 OK")); +} + +TEST_F(DeterministicDispatcherTest, + ErrorsAndDataReadyDispatchedInCreationOrder) { + std::vector<std::string> notifications; + std::unique_ptr<FakeManagedDispatchURLRequestJob> job1( + new FakeManagedDispatchURLRequestJob(deterministic_dispatcher_.get(), 1, + ¬ifications)); + std::unique_ptr<FakeManagedDispatchURLRequestJob> job2( + new FakeManagedDispatchURLRequestJob(deterministic_dispatcher_.get(), 2, + ¬ifications)); + std::unique_ptr<FakeManagedDispatchURLRequestJob> job3( + new FakeManagedDispatchURLRequestJob(deterministic_dispatcher_.get(), 3, + ¬ifications)); + std::unique_ptr<FakeManagedDispatchURLRequestJob> job4( + new FakeManagedDispatchURLRequestJob(deterministic_dispatcher_.get(), 4, + ¬ifications)); + job4->DispatchHeadersComplete(); + job3->DispatchStartError(static_cast<net::Error>(-123)); + job2->DispatchHeadersComplete(); + job1->DispatchHeadersComplete(); + + EXPECT_TRUE(notifications.empty()); + + base::RunLoop().RunUntilIdle(); + EXPECT_THAT(notifications, ElementsAre("id: 1 OK")); + + job1.reset(); + base::RunLoop().RunUntilIdle(); + EXPECT_THAT(notifications, ElementsAre("id: 1 OK", "id: 2 OK")); + + job2.reset(); + base::RunLoop().RunUntilIdle(); + EXPECT_THAT(notifications, + ElementsAre("id: 1 OK", "id: 2 OK", "id: 3 err: -123")); + + job3.reset(); + base::RunLoop().RunUntilIdle(); + EXPECT_THAT(notifications, ElementsAre("id: 1 OK", "id: 2 OK", + "id: 3 err: -123", "id: 4 OK")); +} + +} // namespace headless diff --git a/chromium/headless/public/util/deterministic_http_protocol_handler.cc b/chromium/headless/public/util/deterministic_http_protocol_handler.cc new file mode 100644 index 00000000000..bb2c1117e21 --- /dev/null +++ b/chromium/headless/public/util/deterministic_http_protocol_handler.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 "headless/public/util/deterministic_http_protocol_handler.h" + +#include "base/macros.h" +#include "base/memory/ptr_util.h" +#include "headless/public/headless_browser_context.h" +#include "headless/public/util/deterministic_dispatcher.h" +#include "headless/public/util/generic_url_request_job.h" +#include "headless/public/util/http_url_fetcher.h" +#include "net/url_request/url_request_context.h" +#include "net/url_request/url_request_job_factory_impl.h" + +namespace headless { + +class DeterministicHttpProtocolHandler::NopGenericURLRequestJobDelegate + : public GenericURLRequestJob::Delegate { + public: + NopGenericURLRequestJobDelegate() {} + ~NopGenericURLRequestJobDelegate() override {} + + // GenericURLRequestJob::Delegate methods: + bool BlockOrRewriteRequest( + const GURL& url, + const std::string& method, + const std::string& referrer, + GenericURLRequestJob::RewriteCallback callback) override { + return false; + } + + const GenericURLRequestJob::HttpResponse* MaybeMatchResource( + const GURL& url, + const std::string& method, + const net::HttpRequestHeaders& request_headers) override { + return nullptr; + } + + void OnResourceLoadComplete(const GURL& final_url, + const std::string& mime_type, + int http_response_code) override {} + + private: + DISALLOW_COPY_AND_ASSIGN(NopGenericURLRequestJobDelegate); +}; + +DeterministicHttpProtocolHandler::DeterministicHttpProtocolHandler( + DeterministicDispatcher* deterministic_dispatcher, + scoped_refptr<base::SingleThreadTaskRunner> io_task_runner) + : deterministic_dispatcher_(deterministic_dispatcher), + io_task_runner_(io_task_runner), + nop_delegate_(new NopGenericURLRequestJobDelegate()) {} + +DeterministicHttpProtocolHandler::~DeterministicHttpProtocolHandler() { + if (url_request_context_) + io_task_runner_->DeleteSoon(FROM_HERE, url_request_context_.release()); + if (url_request_job_factory_) + io_task_runner_->DeleteSoon(FROM_HERE, url_request_job_factory_.release()); +} + +net::URLRequestJob* DeterministicHttpProtocolHandler::MaybeCreateJob( + net::URLRequest* request, + net::NetworkDelegate* network_delegate) const { + if (!url_request_context_) { + DCHECK(io_task_runner_->BelongsToCurrentThread()); + // Create our own URLRequestContext with an empty URLRequestJobFactoryImpl + // which lets us use the default http(s) RequestJobs. + url_request_context_.reset(new net::URLRequestContext()); + url_request_context_->CopyFrom(request->context()); + url_request_job_factory_.reset(new net::URLRequestJobFactoryImpl()); + url_request_context_->set_job_factory(url_request_job_factory_.get()); + } + return new GenericURLRequestJob( + request, network_delegate, deterministic_dispatcher_, + base::MakeUnique<HttpURLFetcher>(url_request_context_.get()), + nop_delegate_.get()); +} + +} // namespace headless diff --git a/chromium/headless/public/util/deterministic_http_protocol_handler.h b/chromium/headless/public/util/deterministic_http_protocol_handler.h new file mode 100644 index 00000000000..da0a70d1e4b --- /dev/null +++ b/chromium/headless/public/util/deterministic_http_protocol_handler.h @@ -0,0 +1,59 @@ +// 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 HEADLESS_PUBLIC_UTIL_DETERMINISTIC_HTTP_PROTOCOL_HANDLER_H_ +#define HEADLESS_PUBLIC_UTIL_DETERMINISTIC_HTTP_PROTOCOL_HANDLER_H_ + +#include <memory> + +#include "base/single_thread_task_runner.h" +#include "net/url_request/url_request_job_factory.h" + +namespace net { +class URLRequestContext; +class URLRequestJobFactory; +} // namespace + +namespace headless { +class DeterministicDispatcher; +class HeadlessBrowserContext; + +// A deterministic protocol handler. Requests made to this protocol handler +// will return in order of creation, regardless of what order the network +// returns them in. This helps remove one large source of network related +// non determinism at the cost of slower page loads. +class DeterministicHttpProtocolHandler + : public net::URLRequestJobFactory::ProtocolHandler { + public: + // Note |deterministic_dispatcher| is expected to be shared across a number of + // protocol handlers, e.g. for http & https protocols. + DeterministicHttpProtocolHandler( + DeterministicDispatcher* deterministic_dispatcher, + scoped_refptr<base::SingleThreadTaskRunner> io_task_runner); + ~DeterministicHttpProtocolHandler() override; + + net::URLRequestJob* MaybeCreateJob( + net::URLRequest* request, + net::NetworkDelegate* network_delegate) const override; + + private: + class NopGenericURLRequestJobDelegate; + + DeterministicDispatcher* deterministic_dispatcher_; // NOT OWNED. + scoped_refptr<base::SingleThreadTaskRunner> io_task_runner_; + std::unique_ptr<NopGenericURLRequestJobDelegate> nop_delegate_; + + // |url_request_context_| and |url_request_job_factory_| are lazily created on + // the IO thread. The URLRequestContext is setup to bypass any user-specified + // protocol handlers including this one. This is necessary to actually fetch + // http resources. + mutable std::unique_ptr<net::URLRequestContext> url_request_context_; + mutable std::unique_ptr<net::URLRequestJobFactory> url_request_job_factory_; + + DISALLOW_COPY_AND_ASSIGN(DeterministicHttpProtocolHandler); +}; + +} // namespace headless + +#endif // HEADLESS_PUBLIC_UTIL_DETERMINISTIC_HTTP_PROTOCOL_HANDLER_H_ diff --git a/chromium/headless/public/util/error_reporter.cc b/chromium/headless/public/util/error_reporter.cc index 54c0758b71d..88722d7b79d 100644 --- a/chromium/headless/public/util/error_reporter.cc +++ b/chromium/headless/public/util/error_reporter.cc @@ -24,7 +24,7 @@ void ErrorReporter::Pop() { void ErrorReporter::SetName(const char* name) { DCHECK(!path_.empty()); - path_[path_.size() - 1] = name; + path_.back() = name; } void ErrorReporter::AddError(base::StringPiece description) { diff --git a/chromium/headless/public/util/expedited_dispatcher.cc b/chromium/headless/public/util/expedited_dispatcher.cc new file mode 100644 index 00000000000..0120442dd0e --- /dev/null +++ b/chromium/headless/public/util/expedited_dispatcher.cc @@ -0,0 +1,39 @@ +// 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 "headless/public/util/expedited_dispatcher.h" + +#include <utility> + +#include "base/bind.h" +#include "headless/public/util/managed_dispatch_url_request_job.h" + +namespace headless { + +ExpeditedDispatcher::ExpeditedDispatcher( + scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner) + : io_thread_task_runner_(std::move(io_thread_task_runner)) {} + +ExpeditedDispatcher::~ExpeditedDispatcher() {} + +void ExpeditedDispatcher::JobCreated(ManagedDispatchURLRequestJob*) {} + +void ExpeditedDispatcher::JobKilled(ManagedDispatchURLRequestJob*) {} + +void ExpeditedDispatcher::JobFailed(ManagedDispatchURLRequestJob* job, + net::Error error) { + io_thread_task_runner_->PostTask( + FROM_HERE, base::Bind(&ManagedDispatchURLRequestJob::OnStartError, + base::Unretained(job), error)); +} + +void ExpeditedDispatcher::DataReady(ManagedDispatchURLRequestJob* job) { + io_thread_task_runner_->PostTask( + FROM_HERE, base::Bind(&ManagedDispatchURLRequestJob::OnHeadersComplete, + base::Unretained(job))); +} + +void ExpeditedDispatcher::JobDeleted(ManagedDispatchURLRequestJob*) {} + +} // namespace headless diff --git a/chromium/headless/public/util/expedited_dispatcher.h b/chromium/headless/public/util/expedited_dispatcher.h new file mode 100644 index 00000000000..39b8057fdfc --- /dev/null +++ b/chromium/headless/public/util/expedited_dispatcher.h @@ -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. + +#ifndef HEADLESS_PUBLIC_UTIL_EXPEDITED_DISPATCHER_H_ +#define HEADLESS_PUBLIC_UTIL_EXPEDITED_DISPATCHER_H_ + +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/single_thread_task_runner.h" +#include "headless/public/util/url_request_dispatcher.h" +#include "net/base/net_errors.h" + +namespace headless { + +class ManagedDispatchURLRequestJob; + +// This class dispatches calls to OnHeadersComplete and OnStartError +// immediately sacrificing determinism for performance. +class ExpeditedDispatcher : public URLRequestDispatcher { + public: + explicit ExpeditedDispatcher( + scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner); + ~ExpeditedDispatcher() override; + + // UrlRequestDispatcher implementation: + void JobCreated(ManagedDispatchURLRequestJob* job) override; + void JobKilled(ManagedDispatchURLRequestJob* job) override; + void JobFailed(ManagedDispatchURLRequestJob* job, net::Error error) override; + void DataReady(ManagedDispatchURLRequestJob* job) override; + void JobDeleted(ManagedDispatchURLRequestJob* job) override; + + private: + scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner_; + + DISALLOW_COPY_AND_ASSIGN(ExpeditedDispatcher); +}; + +} // namespace headless + +#endif // HEADLESS_PUBLIC_UTIL_EXPEDITED_DISPATCHER_H_ diff --git a/chromium/headless/public/util/expedited_dispatcher_test.cc b/chromium/headless/public/util/expedited_dispatcher_test.cc new file mode 100644 index 00000000000..5ef6301ba1d --- /dev/null +++ b/chromium/headless/public/util/expedited_dispatcher_test.cc @@ -0,0 +1,86 @@ +// 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 "headless/public/util/expedited_dispatcher.h" + +#include <memory> +#include <string> +#include <vector> + +#include "base/run_loop.h" +#include "base/single_thread_task_runner.h" +#include "headless/public/util/testing/fake_managed_dispatch_url_request_job.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::ElementsAre; + +namespace headless { + +class ExpeditedDispatcherTest : public ::testing::Test { + protected: + ExpeditedDispatcherTest() {} + ~ExpeditedDispatcherTest() override {} + + void SetUp() override { + expedited_dispatcher_.reset(new ExpeditedDispatcher(loop_.task_runner())); + } + + base::MessageLoop loop_; + std::unique_ptr<ExpeditedDispatcher> expedited_dispatcher_; +}; + +TEST_F(ExpeditedDispatcherTest, DispatchDataReadyInReverseOrder) { + std::vector<std::string> notifications; + std::unique_ptr<FakeManagedDispatchURLRequestJob> job1( + new FakeManagedDispatchURLRequestJob(expedited_dispatcher_.get(), 1, + ¬ifications)); + std::unique_ptr<FakeManagedDispatchURLRequestJob> job2( + new FakeManagedDispatchURLRequestJob(expedited_dispatcher_.get(), 2, + ¬ifications)); + std::unique_ptr<FakeManagedDispatchURLRequestJob> job3( + new FakeManagedDispatchURLRequestJob(expedited_dispatcher_.get(), 3, + ¬ifications)); + std::unique_ptr<FakeManagedDispatchURLRequestJob> job4( + new FakeManagedDispatchURLRequestJob(expedited_dispatcher_.get(), 4, + ¬ifications)); + job4->DispatchHeadersComplete(); + job3->DispatchHeadersComplete(); + job2->DispatchHeadersComplete(); + job1->DispatchHeadersComplete(); + + EXPECT_TRUE(notifications.empty()); + + base::RunLoop().RunUntilIdle(); + EXPECT_THAT(notifications, + ElementsAre("id: 4 OK", "id: 3 OK", "id: 2 OK", "id: 1 OK")); +} + +TEST_F(ExpeditedDispatcherTest, ErrorsAndDataReadyDispatchedInCallOrder) { + std::vector<std::string> notifications; + std::unique_ptr<FakeManagedDispatchURLRequestJob> job1( + new FakeManagedDispatchURLRequestJob(expedited_dispatcher_.get(), 1, + ¬ifications)); + std::unique_ptr<FakeManagedDispatchURLRequestJob> job2( + new FakeManagedDispatchURLRequestJob(expedited_dispatcher_.get(), 2, + ¬ifications)); + std::unique_ptr<FakeManagedDispatchURLRequestJob> job3( + new FakeManagedDispatchURLRequestJob(expedited_dispatcher_.get(), 3, + ¬ifications)); + std::unique_ptr<FakeManagedDispatchURLRequestJob> job4( + new FakeManagedDispatchURLRequestJob(expedited_dispatcher_.get(), 4, + ¬ifications)); + job4->DispatchHeadersComplete(); + job3->DispatchStartError(static_cast<net::Error>(-123)); + job2->DispatchHeadersComplete(); + job1->DispatchHeadersComplete(); + + EXPECT_TRUE(notifications.empty()); + + base::RunLoop().RunUntilIdle(); + EXPECT_THAT(notifications, ElementsAre("id: 4 OK", "id: 3 err: -123", + "id: 2 OK", "id: 1 OK")); +} + +} // namespace headless diff --git a/chromium/headless/public/util/generic_url_request_job.cc b/chromium/headless/public/util/generic_url_request_job.cc new file mode 100644 index 00000000000..9c7245bc430 --- /dev/null +++ b/chromium/headless/public/util/generic_url_request_job.cc @@ -0,0 +1,189 @@ +// 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 "headless/public/util/generic_url_request_job.h" + +#include <string.h> +#include <algorithm> + +#include "base/logging.h" +#include "headless/public/util/url_request_dispatcher.h" +#include "net/base/io_buffer.h" +#include "net/base/net_errors.h" +#include "net/base/registry_controlled_domains/registry_controlled_domain.h" +#include "net/cookies/cookie_store.h" +#include "net/http/http_response_headers.h" +#include "net/url_request/url_request_context.h" + +namespace headless { +namespace { + +// True if the request method is "safe" (per section 4.2.1 of RFC 7231). +bool IsMethodSafe(const std::string& method) { + return method == "GET" || method == "HEAD" || method == "OPTIONS" || + method == "TRACE"; +} + +} // namespace + +GenericURLRequestJob::GenericURLRequestJob( + net::URLRequest* request, + net::NetworkDelegate* network_delegate, + URLRequestDispatcher* url_request_dispatcher, + std::unique_ptr<URLFetcher> url_fetcher, + Delegate* delegate) + : ManagedDispatchURLRequestJob(request, + network_delegate, + url_request_dispatcher), + url_fetcher_(std::move(url_fetcher)), + delegate_(delegate), + weak_factory_(this) {} + +GenericURLRequestJob::~GenericURLRequestJob() = default; + +void GenericURLRequestJob::SetExtraRequestHeaders( + const net::HttpRequestHeaders& headers) { + extra_request_headers_ = headers; +} + +void GenericURLRequestJob::Start() { + auto callback = [this](RewriteResult result, const GURL& url, + const std::string& method) { + switch (result) { + case RewriteResult::kAllow: + // Note that we use the rewritten url for selecting cookies. + // Also, rewriting does not affect the request initiator. + PrepareCookies(url, method, url::Origin(url)); + break; + case RewriteResult::kDeny: + DispatchStartError(net::ERR_FILE_NOT_FOUND); + break; + case RewriteResult::kFailure: + DispatchStartError(net::ERR_UNEXPECTED); + break; + default: + DCHECK(false); + } + }; + + if (!delegate_->BlockOrRewriteRequest(request_->url(), request_->method(), + request_->referrer(), callback)) { + PrepareCookies(request_->url(), request_->method(), + url::Origin(request_->first_party_for_cookies())); + } +} + +void GenericURLRequestJob::PrepareCookies(const GURL& rewritten_url, + const std::string& method, + const url::Origin& site_for_cookies) { + net::CookieStore* cookie_store = request_->context()->cookie_store(); + net::CookieOptions options; + options.set_include_httponly(); + + // See net::URLRequestHttpJob::AddCookieHeaderAndStart(). + url::Origin requested_origin(rewritten_url); + if (net::registry_controlled_domains::SameDomainOrHost( + requested_origin, site_for_cookies, + net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES)) { + if (net::registry_controlled_domains::SameDomainOrHost( + requested_origin, request_->initiator(), + net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES)) { + options.set_same_site_cookie_mode( + net::CookieOptions::SameSiteCookieMode::INCLUDE_STRICT_AND_LAX); + } else if (IsMethodSafe(request_->method())) { + options.set_same_site_cookie_mode( + net::CookieOptions::SameSiteCookieMode::INCLUDE_LAX); + } + } + + cookie_store->GetCookieListWithOptionsAsync( + rewritten_url, options, + base::Bind(&GenericURLRequestJob::OnCookiesAvailable, + weak_factory_.GetWeakPtr(), rewritten_url, method)); +} + +void GenericURLRequestJob::OnCookiesAvailable( + const GURL& rewritten_url, + const std::string& method, + const net::CookieList& cookie_list) { + // TODO(alexclarke): Set user agent. + // Pass cookies, the referrer and any extra headers into the fetch request. + extra_request_headers_.SetHeader( + net::HttpRequestHeaders::kCookie, + net::CookieStore::BuildCookieLine(cookie_list)); + + extra_request_headers_.SetHeader(net::HttpRequestHeaders::kReferer, + request_->referrer()); + + // The resource may have been supplied in the request. + const HttpResponse* matched_resource = delegate_->MaybeMatchResource( + rewritten_url, method, extra_request_headers_); + + if (matched_resource) { + OnFetchCompleteExtractHeaders( + matched_resource->final_url, matched_resource->http_response_code, + matched_resource->response_data, matched_resource->response_data_size); + } else { + url_fetcher_->StartFetch(rewritten_url, method, extra_request_headers_, + this); + } +} + +void GenericURLRequestJob::OnFetchStartError(net::Error error) { + DispatchStartError(error); +} + +void GenericURLRequestJob::OnFetchComplete( + const GURL& final_url, + int http_response_code, + scoped_refptr<net::HttpResponseHeaders> response_headers, + const char* body, + size_t body_size) { + http_response_code_ = http_response_code; + response_headers_ = response_headers; + body_ = body; + body_size_ = body_size; + + DispatchHeadersComplete(); + + std::string mime_type; + GetMimeType(&mime_type); + + delegate_->OnResourceLoadComplete(final_url, mime_type, http_response_code); +} + +int GenericURLRequestJob::ReadRawData(net::IOBuffer* buf, int buf_size) { + // TODO(skyostil): Implement ranged fetches. + // TODO(alexclarke): Add coverage for all the cases below. + size_t bytes_available = body_size_ - read_offset_; + size_t bytes_to_copy = + std::min(static_cast<size_t>(buf_size), bytes_available); + if (bytes_to_copy) { + std::memcpy(buf->data(), &body_[read_offset_], bytes_to_copy); + read_offset_ += bytes_to_copy; + } + return bytes_to_copy; +} + +int GenericURLRequestJob::GetResponseCode() const { + return http_response_code_; +} + +void GenericURLRequestJob::GetResponseInfo(net::HttpResponseInfo* info) { + info->headers = response_headers_; +} + +bool GenericURLRequestJob::GetMimeType(std::string* mime_type) const { + if (!response_headers_) + return false; + return response_headers_->GetMimeType(mime_type); +} + +bool GenericURLRequestJob::GetCharset(std::string* charset) { + if (!response_headers_) + return false; + return response_headers_->GetCharset(charset); +} + +} // namespace headless diff --git a/chromium/headless/public/util/generic_url_request_job.h b/chromium/headless/public/util/generic_url_request_job.h new file mode 100644 index 00000000000..514a5e176f9 --- /dev/null +++ b/chromium/headless/public/util/generic_url_request_job.h @@ -0,0 +1,132 @@ +// 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 HEADLESS_PUBLIC_UTIL_GENERIC_URL_REQUEST_JOB_H_ +#define HEADLESS_PUBLIC_UTIL_GENERIC_URL_REQUEST_JOB_H_ + +#include <stddef.h> +#include <functional> +#include <memory> +#include <string> + +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "headless/public/util/managed_dispatch_url_request_job.h" +#include "headless/public/util/url_fetcher.h" +#include "net/base/net_errors.h" +#include "net/url_request/url_request.h" +#include "url/gurl.h" + +namespace net { +class HttpResponseHeaders; +class IOBuffer; +} // namespace net + +namespace headless { + +class URLRequestDispatcher; + +// Intended for use in a protocol handler, this ManagedDispatchURLRequestJob has +// the following features: +// +// 1. The delegate can extension observe / cancel and redirect requests +// 2. The delegate can optionally provide the results, otherwise the specifed +// fetcher is invoked. +class GenericURLRequestJob : public ManagedDispatchURLRequestJob, + public URLFetcher::ResultListener { + public: + enum class RewriteResult { kAllow, kDeny, kFailure }; + using RewriteCallback = std::function< + void(RewriteResult result, const GURL& url, const std::string& method)>; + + struct HttpResponse { + GURL final_url; + int http_response_code; + + // The HTTP headers and response body. Note the lifetime of |response_data| + // is expected to outlive the GenericURLRequestJob. + const char* response_data; // NOT OWNED + size_t response_data_size; + }; + + class Delegate { + public: + // Allows the delegate to rewrite the URL for a given request. Return true + // to signal that the rewrite is in progress and |callback| will be called + // with the result, or false to indicate that no rewriting is necessary. + // Called on an arbitrary thread. + virtual bool BlockOrRewriteRequest(const GURL& url, + const std::string& method, + const std::string& referrer, + RewriteCallback callback) = 0; + + // Allows the delegate to synchronously fulfill a request with a reply. + // Called on an arbitrary thread. + virtual const HttpResponse* MaybeMatchResource( + const GURL& url, + const std::string& method, + const net::HttpRequestHeaders& request_headers) = 0; + + // Signals that a resource load has finished. Called on an arbitrary thread. + virtual void OnResourceLoadComplete(const GURL& final_url, + const std::string& mime_type, + int http_response_code) = 0; + + protected: + virtual ~Delegate() {} + }; + + // NOTE |url_request_dispatcher| and |delegate| must outlive the + // GenericURLRequestJob. + GenericURLRequestJob(net::URLRequest* request, + net::NetworkDelegate* network_delegate, + URLRequestDispatcher* url_request_dispatcher, + std::unique_ptr<URLFetcher> url_fetcher, + Delegate* delegate); + ~GenericURLRequestJob() override; + + // net::URLRequestJob implementation: + void SetExtraRequestHeaders(const net::HttpRequestHeaders& headers) override; + void Start() override; + int ReadRawData(net::IOBuffer* buf, int buf_size) override; + int GetResponseCode() const override; + void GetResponseInfo(net::HttpResponseInfo* info) override; + bool GetMimeType(std::string* mime_type) const override; + bool GetCharset(std::string* charset) override; + + // URLFetcher::FetchResultListener implementation: + void OnFetchStartError(net::Error error) override; + void OnFetchComplete(const GURL& final_url, + int http_response_code, + scoped_refptr<net::HttpResponseHeaders> response_headers, + const char* body, + size_t body_size) override; + + private: + void PrepareCookies(const GURL& rewritten_url, + const std::string& method, + const url::Origin& site_for_cookies); + + void OnCookiesAvailable(const GURL& rewritten_url, + const std::string& method, + const net::CookieList& cookie_list); + + std::unique_ptr<URLFetcher> url_fetcher_; + net::HttpRequestHeaders extra_request_headers_; + scoped_refptr<net::HttpResponseHeaders> response_headers_; + Delegate* delegate_; // Not owned. + const char* body_ = nullptr; // Not owned. + int http_response_code_ = 0; + size_t body_size_ = 0; + size_t read_offset_ = 0; + + base::WeakPtrFactory<GenericURLRequestJob> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(GenericURLRequestJob); +}; + +} // namespace headless + +#endif // HEADLESS_PUBLIC_UTIL_GENERIC_URL_REQUEST_JOB_H_ diff --git a/chromium/headless/public/util/generic_url_request_job_test.cc b/chromium/headless/public/util/generic_url_request_job_test.cc new file mode 100644 index 00000000000..989b5f61638 --- /dev/null +++ b/chromium/headless/public/util/generic_url_request_job_test.cc @@ -0,0 +1,366 @@ +// 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 "headless/public/util/generic_url_request_job.h" + +#include <memory> +#include <string> +#include <vector> + +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/memory/ptr_util.h" +#include "base/run_loop.h" +#include "base/single_thread_task_runner.h" +#include "base/strings/stringprintf.h" +#include "base/values.h" +#include "headless/public/util/expedited_dispatcher.h" +#include "headless/public/util/testing/generic_url_request_mocks.h" +#include "headless/public/util/url_fetcher.h" +#include "net/http/http_response_headers.h" +#include "net/url_request/url_request_job_factory_impl.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +std::ostream& operator<<(std::ostream& os, const base::Value& value) { + std::string json; + base::JSONWriter::WriteWithOptions( + value, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json); + os << json; + return os; +} + +MATCHER_P(MatchesJson, json, json.c_str()) { + std::unique_ptr<base::Value> expected( + base::JSONReader::Read(json, base::JSON_PARSE_RFC)); + return arg.Equals(expected.get()); +} + +namespace headless { + +namespace { + +class MockFetcher : public URLFetcher { + public: + MockFetcher(base::DictionaryValue* fetch_request, + const std::string& json_reply) + : fetch_reply_(base::JSONReader::Read(json_reply, base::JSON_PARSE_RFC)), + fetch_request_(fetch_request) { + CHECK(fetch_reply_) << "Invalid json: " << json_reply; + } + + ~MockFetcher() override {} + + void StartFetch(const GURL& url, + const std::string& method, + const net::HttpRequestHeaders& request_headers, + ResultListener* result_listener) override { + // Record the request. + fetch_request_->SetString("url", url.spec()); + fetch_request_->SetString("method", method); + std::unique_ptr<base::DictionaryValue> headers(new base::DictionaryValue); + for (net::HttpRequestHeaders::Iterator it(request_headers); it.GetNext();) { + headers->SetString(it.name(), it.value()); + } + fetch_request_->Set("headers", std::move(headers)); + + // Return the canned response. + base::DictionaryValue* reply_dictionary; + ASSERT_TRUE(fetch_reply_->GetAsDictionary(&reply_dictionary)); + std::string final_url; + ASSERT_TRUE(reply_dictionary->GetString("url", &final_url)); + int http_response_code; + ASSERT_TRUE(reply_dictionary->GetInteger("http_response_code", + &http_response_code)); + ASSERT_TRUE(reply_dictionary->GetString("data", &response_data_)); + base::DictionaryValue* reply_headers_dictionary; + ASSERT_TRUE( + reply_dictionary->GetDictionary("headers", &reply_headers_dictionary)); + scoped_refptr<net::HttpResponseHeaders> response_headers( + new net::HttpResponseHeaders("")); + for (base::DictionaryValue::Iterator it(*reply_headers_dictionary); + !it.IsAtEnd(); it.Advance()) { + std::string value; + ASSERT_TRUE(it.value().GetAsString(&value)); + response_headers->AddHeader( + base::StringPrintf("%s: %s", it.key().c_str(), value.c_str())); + } + result_listener->OnFetchComplete( + GURL(final_url), http_response_code, std::move(response_headers), + response_data_.c_str(), response_data_.size()); + } + + private: + std::unique_ptr<base::Value> fetch_reply_; + base::DictionaryValue* fetch_request_; // NOT OWNED + std::string response_data_; // Here to ensure the required lifetime. +}; + +class MockProtocolHandler : public net::URLRequestJobFactory::ProtocolHandler { + public: + // Details of the fetch will be stored in |fetch_request|. + // The fetch response will be created from parsing |json_fetch_reply_|. + MockProtocolHandler(base::DictionaryValue* fetch_request, + std::string* json_fetch_reply, + URLRequestDispatcher* dispatcher, + GenericURLRequestJob::Delegate* job_delegate) + : fetch_request_(fetch_request), + json_fetch_reply_(json_fetch_reply), + job_delegate_(job_delegate), + dispatcher_(dispatcher) {} + + // net::URLRequestJobFactory::ProtocolHandler override. + net::URLRequestJob* MaybeCreateJob( + net::URLRequest* request, + net::NetworkDelegate* network_delegate) const override { + return new GenericURLRequestJob( + request, network_delegate, dispatcher_, + base::MakeUnique<MockFetcher>(fetch_request_, *json_fetch_reply_), + job_delegate_); + } + + private: + base::DictionaryValue* fetch_request_; // NOT OWNED + std::string* json_fetch_reply_; // NOT OWNED + GenericURLRequestJob::Delegate* job_delegate_; // NOT OWNED + URLRequestDispatcher* dispatcher_; // NOT OWNED +}; + +} // namespace + +class GenericURLRequestJobTest : public testing::Test { + public: + GenericURLRequestJobTest() : dispatcher_(message_loop_.task_runner()) { + url_request_job_factory_.SetProtocolHandler( + "https", base::WrapUnique(new MockProtocolHandler( + &fetch_request_, &json_fetch_reply_, &dispatcher_, + &job_delegate_))); + url_request_context_.set_job_factory(&url_request_job_factory_); + url_request_context_.set_cookie_store(&cookie_store_); + } + + std::unique_ptr<net::URLRequest> CreateAndCompleteJob( + const GURL& url, + const std::string& json_reply) { + json_fetch_reply_ = json_reply; + + std::unique_ptr<net::URLRequest> request(url_request_context_.CreateRequest( + url, net::DEFAULT_PRIORITY, &request_delegate_)); + request->Start(); + base::RunLoop().RunUntilIdle(); + return request; + } + + protected: + base::MessageLoop message_loop_; + ExpeditedDispatcher dispatcher_; + + net::URLRequestJobFactoryImpl url_request_job_factory_; + net::URLRequestContext url_request_context_; + MockCookieStore cookie_store_; + + MockURLRequestDelegate request_delegate_; + base::DictionaryValue fetch_request_; // The request sent to MockFetcher. + std::string json_fetch_reply_; // The reply to be sent by MockFetcher. + MockGenericURLRequestJobDelegate job_delegate_; +}; + +TEST_F(GenericURLRequestJobTest, BasicRequestParams) { + // TODO(alexclarke): Lobby for raw string literals and use them here! + json_fetch_reply_ = + "{\"url\":\"https://example.com\"," + " \"http_response_code\":200," + " \"data\":\"Reply\"," + " \"headers\":{\"Content-Type\":\"text/plain\"}}"; + + std::unique_ptr<net::URLRequest> request(url_request_context_.CreateRequest( + GURL("https://example.com"), net::DEFAULT_PRIORITY, &request_delegate_)); + request->SetReferrer("https://referrer.example.com"); + request->SetExtraRequestHeaderByName("Extra-Header", "Value", true); + request->SetExtraRequestHeaderByName("User-Agent", "TestBrowser", true); + request->SetExtraRequestHeaderByName("Accept", "text/plain", true); + request->Start(); + base::RunLoop().RunUntilIdle(); + + std::string expected_request_json = + "{\"url\": \"https://example.com/\"," + " \"method\": \"GET\"," + " \"headers\": {" + " \"Accept\": \"text/plain\"," + " \"Cookie\": \"\"," + " \"Extra-Header\": \"Value\"," + " \"Referer\": \"https://referrer.example.com/\"," + " \"User-Agent\": \"TestBrowser\"" + " }" + "}"; + + EXPECT_THAT(fetch_request_, MatchesJson(expected_request_json)); +} + +TEST_F(GenericURLRequestJobTest, BasicRequestProperties) { + std::string reply = + "{\"url\":\"https://example.com\"," + " \"http_response_code\":200," + " \"data\":\"Reply\"," + " \"headers\":{\"Content-Type\":\"text/html; charset=UTF-8\"}}"; + + std::unique_ptr<net::URLRequest> request( + CreateAndCompleteJob(GURL("https://example.com"), reply)); + + EXPECT_EQ(200, request->GetResponseCode()); + + std::string mime_type; + request->GetMimeType(&mime_type); + EXPECT_EQ("text/html", mime_type); + + std::string charset; + request->GetCharset(&charset); + EXPECT_EQ("utf-8", charset); + + std::string content_type; + EXPECT_TRUE(request->response_info().headers->GetNormalizedHeader( + "Content-Type", &content_type)); + EXPECT_EQ("text/html; charset=UTF-8", content_type); +} + +TEST_F(GenericURLRequestJobTest, BasicRequestContents) { + std::string reply = + "{\"url\":\"https://example.com\"," + " \"http_response_code\":200," + " \"data\":\"Reply\"," + " \"headers\":{\"Content-Type\":\"text/html; charset=UTF-8\"}}"; + + std::unique_ptr<net::URLRequest> request( + CreateAndCompleteJob(GURL("https://example.com"), reply)); + + const int kBufferSize = 256; + scoped_refptr<net::IOBuffer> buffer(new net::IOBuffer(kBufferSize)); + int bytes_read; + EXPECT_TRUE(request->Read(buffer.get(), kBufferSize, &bytes_read)); + EXPECT_EQ(5, bytes_read); + EXPECT_EQ("Reply", std::string(buffer->data(), 5)); +} + +TEST_F(GenericURLRequestJobTest, ReadInParts) { + std::string reply = + "{\"url\":\"https://example.com\"," + " \"http_response_code\":200," + " \"data\":\"Reply\"," + " \"headers\":{\"Content-Type\":\"text/html; charset=UTF-8\"}}"; + + std::unique_ptr<net::URLRequest> request( + CreateAndCompleteJob(GURL("https://example.com"), reply)); + + const int kBufferSize = 3; + scoped_refptr<net::IOBuffer> buffer(new net::IOBuffer(kBufferSize)); + int bytes_read; + EXPECT_TRUE(request->Read(buffer.get(), kBufferSize, &bytes_read)); + EXPECT_EQ(3, bytes_read); + EXPECT_EQ("Rep", std::string(buffer->data(), bytes_read)); + + EXPECT_TRUE(request->Read(buffer.get(), kBufferSize, &bytes_read)); + EXPECT_EQ(2, bytes_read); + EXPECT_EQ("ly", std::string(buffer->data(), bytes_read)); + + EXPECT_TRUE(request->Read(buffer.get(), kBufferSize, &bytes_read)); + EXPECT_EQ(0, bytes_read); +} + +TEST_F(GenericURLRequestJobTest, RequestWithCookies) { + net::CookieList* cookies = cookie_store_.cookies(); + + // Basic matching cookie. + cookies->push_back(*net::CanonicalCookie::Create( + GURL("https://example.com"), "basic_cookie", "1", "example.com", "/", + base::Time(), base::Time(), + /* secure */ false, + /* http_only */ false, net::CookieSameSite::NO_RESTRICTION, + /* enforce_strict_secure */ false, net::COOKIE_PRIORITY_DEFAULT)); + + // Matching secure cookie. + cookies->push_back(*net::CanonicalCookie::Create( + GURL("https://example.com"), "secure_cookie", "2", "example.com", "/", + base::Time(), base::Time(), + /* secure */ true, + /* http_only */ false, net::CookieSameSite::NO_RESTRICTION, + /* enforce_strict_secure */ true, net::COOKIE_PRIORITY_DEFAULT)); + + // Matching http-only cookie. + cookies->push_back(*net::CanonicalCookie::Create( + GURL("https://example.com"), "http_only_cookie", "3", "example.com", "/", + base::Time(), base::Time(), + /* secure */ false, + /* http_only */ true, net::CookieSameSite::NO_RESTRICTION, + /* enforce_strict_secure */ false, net::COOKIE_PRIORITY_DEFAULT)); + + // Matching cookie with path. + cookies->push_back(*net::CanonicalCookie::Create( + GURL("https://example.com"), "cookie_with_path", "4", "example.com", + "/widgets", base::Time(), base::Time(), + /* secure */ false, + /* http_only */ false, net::CookieSameSite::NO_RESTRICTION, + /* enforce_strict_secure */ false, net::COOKIE_PRIORITY_DEFAULT)); + + // Matching cookie with subdomain. + cookies->push_back(*net::CanonicalCookie::Create( + GURL("https://cdn.example.com"), "bad_subdomain_cookie", "5", + "cdn.example.com", "/", base::Time(), base::Time(), + /* secure */ false, + /* http_only */ false, net::CookieSameSite::NO_RESTRICTION, + /* enforce_strict_secure */ false, net::COOKIE_PRIORITY_DEFAULT)); + + // Non-matching cookie (different site). + cookies->push_back(*net::CanonicalCookie::Create( + GURL("https://zombo.com"), "bad_site_cookie", "6", "zombo.com", "/", + base::Time(), base::Time(), + /* secure */ false, + /* http_only */ false, net::CookieSameSite::NO_RESTRICTION, + /* enforce_strict_secure */ false, net::COOKIE_PRIORITY_DEFAULT)); + + // Non-matching cookie (different path). + cookies->push_back(*net::CanonicalCookie::Create( + GURL("https://example.com"), "bad_path_cookie", "7", "example.com", + "/gadgets", base::Time(), base::Time(), + /* secure */ false, + /* http_only */ false, net::CookieSameSite::NO_RESTRICTION, + /* enforce_strict_secure */ false, net::COOKIE_PRIORITY_DEFAULT)); + + std::string reply = + "{\"url\":\"https://example.com\"," + " \"http_response_code\":200," + " \"data\":\"Reply\"," + " \"headers\":{\"Content-Type\":\"text/html; charset=UTF-8\"}}"; + + std::unique_ptr<net::URLRequest> request( + CreateAndCompleteJob(GURL("https://example.com"), reply)); + + std::string expected_request_json = + "{\"url\": \"https://example.com/\"," + " \"method\": \"GET\"," + " \"headers\": {" + " \"Cookie\": \"basic_cookie=1; secure_cookie=2; http_only_cookie=3\"," + " \"Referer\": \"\"" + " }" + "}"; + + EXPECT_THAT(fetch_request_, MatchesJson(expected_request_json)); +} + +TEST_F(GenericURLRequestJobTest, DelegateBlocksLoading) { + std::string reply = + "{\"url\":\"https://example.com\"," + " \"http_response_code\":200," + " \"data\":\"Reply\"," + " \"headers\":{\"Content-Type\":\"text/html; charset=UTF-8\"}}"; + + job_delegate_.SetShouldBlock(true); + + std::unique_ptr<net::URLRequest> request( + CreateAndCompleteJob(GURL("https://example.com"), reply)); + + EXPECT_EQ(net::URLRequestStatus::FAILED, request->status().status()); + EXPECT_EQ(net::ERR_FILE_NOT_FOUND, request->status().error()); +} + +} // namespace headless diff --git a/chromium/headless/public/util/http_url_fetcher.cc b/chromium/headless/public/util/http_url_fetcher.cc new file mode 100644 index 00000000000..cbb2c0ad667 --- /dev/null +++ b/chromium/headless/public/util/http_url_fetcher.cc @@ -0,0 +1,187 @@ +// 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 "headless/public/util/http_url_fetcher.h" + +#include "net/base/io_buffer.h" +#include "net/cert/cert_status_flags.h" +#include "net/url_request/url_request.h" +#include "net/url_request/url_request_context.h" + +namespace headless { + +class HttpURLFetcher::Delegate : public net::URLRequest::Delegate { + public: + Delegate(const GURL& rewritten_url, + const std::string& method, + const net::HttpRequestHeaders& request_headers, + const net::URLRequestContext* url_request_context, + ResultListener* result_listener); + ~Delegate() override; + + // URLRequest::Delegate methods: + void OnAuthRequired(net::URLRequest* request, + net::AuthChallengeInfo* auth_info) override; + void OnSSLCertificateError(net::URLRequest* request, + const net::SSLInfo& ssl_info, + bool fatal) override; + void OnResponseStarted(net::URLRequest* request, int net_error) override; + void OnReadCompleted(net::URLRequest* request, int num_bytes) override; + + private: + enum { kBufSize = 4096 }; + + bool ConsumeBytesRead(net::URLRequest* request, int num_bytes); + void ReadBody(net::URLRequest* request); + void OnResponseCompleted(net::URLRequest* request, int net_error); + + // Holds the error condition that was hit by the request, or OK. + int result_code_; + + // Buffer that URLRequest writes into. + scoped_refptr<net::IOBuffer> buf_; + + // The HTTP fetch. + std::unique_ptr<net::URLRequest> request_; + + // Holds the bytes read so far. + std::string bytes_read_so_far_; + + // The interface kn which to report any results. + ResultListener* result_listener_; // NOT OWNED. +}; + +HttpURLFetcher::Delegate::Delegate( + const GURL& rewritten_url, + const std::string& method, + const net::HttpRequestHeaders& request_headers, + const net::URLRequestContext* url_request_context, + ResultListener* result_listener) + : result_code_(net::OK), + buf_(new net::IOBuffer(kBufSize)), + request_(url_request_context->CreateRequest(rewritten_url, + net::DEFAULT_PRIORITY, + this)), + result_listener_(result_listener) { + request_->set_method(method); + request_->SetExtraRequestHeaders(request_headers); + request_->Start(); +} + +HttpURLFetcher::Delegate::~Delegate() {} + +void HttpURLFetcher::Delegate::OnAuthRequired( + net::URLRequest* request, + net::AuthChallengeInfo* auth_info) { + DCHECK_EQ(request, request_.get()); + LOG(WARNING) << "Auth required to fetch URL, aborting."; + result_code_ = net::ERR_NOT_IMPLEMENTED; + request->CancelAuth(); +} + +void HttpURLFetcher::Delegate::OnSSLCertificateError( + net::URLRequest* request, + const net::SSLInfo& ssl_info, + bool fatal) { + DCHECK_EQ(request, request_.get()); + + // Revocation check failures are not fatal. + if (net::IsCertStatusMinorError(ssl_info.cert_status)) { + request->ContinueDespiteLastError(); + return; + } + LOG(WARNING) << "SSL certificate error, aborting."; + + // Certificate errors are in same space as net errors. + result_code_ = net::MapCertStatusToNetError(ssl_info.cert_status); + request->Cancel(); +} + +void HttpURLFetcher::Delegate::OnResponseStarted(net::URLRequest* request, + int net_error) { + DCHECK_EQ(request, request_.get()); + DCHECK_NE(net::ERR_IO_PENDING, net_error); + + if (net_error != net::OK) { + OnResponseCompleted(request, net_error); + return; + } + + ReadBody(request); +} + +void HttpURLFetcher::Delegate::OnReadCompleted(net::URLRequest* request, + int num_bytes) { + DCHECK_EQ(request, request_.get()); + DCHECK_NE(net::ERR_IO_PENDING, num_bytes); + + if (ConsumeBytesRead(request, num_bytes)) { + // Keep reading. + ReadBody(request); + } +} + +void HttpURLFetcher::Delegate::ReadBody(net::URLRequest* request) { + // Read as many bytes as are available synchronously. + while (true) { + int num_bytes = request->Read(buf_.get(), kBufSize); + if (num_bytes == net::ERR_IO_PENDING) + return; + + if (num_bytes < 0) { + OnResponseCompleted(request, num_bytes); + return; + } + + if (!ConsumeBytesRead(request, num_bytes)) + return; + } +} + +bool HttpURLFetcher::Delegate::ConsumeBytesRead(net::URLRequest* request, + int num_bytes) { + if (num_bytes <= 0) { + // Error while reading, or EOF. + OnResponseCompleted(request, num_bytes); + return false; + } + + bytes_read_so_far_.append(buf_->data(), num_bytes); + return true; +} + +void HttpURLFetcher::Delegate::OnResponseCompleted(net::URLRequest* request, + int net_error) { + DCHECK_EQ(request, request_.get()); + + if (result_code_ != net::OK) { + result_listener_->OnFetchStartError(static_cast<net::Error>(result_code_)); + return; + } + + if (net_error != net::OK) { + result_listener_->OnFetchStartError(static_cast<net::Error>(net_error)); + return; + } + + result_listener_->OnFetchCompleteExtractHeaders( + request->url(), request->GetResponseCode(), bytes_read_so_far_.c_str(), + bytes_read_so_far_.size()); +} + +HttpURLFetcher::HttpURLFetcher( + const net::URLRequestContext* url_request_context) + : url_request_context_(url_request_context) {} + +HttpURLFetcher::~HttpURLFetcher() {} + +void HttpURLFetcher::StartFetch(const GURL& rewritten_url, + const std::string& method, + const net::HttpRequestHeaders& request_headers, + ResultListener* result_listener) { + delegate_.reset(new Delegate(rewritten_url, method, request_headers, + url_request_context_, result_listener)); +} + +} // namespace headless diff --git a/chromium/headless/public/util/http_url_fetcher.h b/chromium/headless/public/util/http_url_fetcher.h new file mode 100644 index 00000000000..2b17287d8fd --- /dev/null +++ b/chromium/headless/public/util/http_url_fetcher.h @@ -0,0 +1,42 @@ +// 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 HEADLESS_PUBLIC_UTIL_HTTP_URL_FETCHER_H_ +#define HEADLESS_PUBLIC_UTIL_HTTP_URL_FETCHER_H_ + +#include "base/macros.h" +#include "headless/public/util/url_fetcher.h" + +namespace net { +class URLRequestContext; +class URLRequestJobFactory; +} // namespace + +namespace headless { + +// A simple URLFetcher that uses a net::URLRequestContext to as a back end for +// http(s) fetches. +class HttpURLFetcher : public URLFetcher { + public: + explicit HttpURLFetcher(const net::URLRequestContext* url_request_context); + ~HttpURLFetcher() override; + + // URLFetcher implementation: + void StartFetch(const GURL& rewritten_url, + const std::string& method, + const net::HttpRequestHeaders& request_headers, + ResultListener* result_listener) override; + + private: + class Delegate; + + const net::URLRequestContext* url_request_context_; + std::unique_ptr<Delegate> delegate_; + + DISALLOW_COPY_AND_ASSIGN(HttpURLFetcher); +}; + +} // namespace headless + +#endif // HEADLESS_PUBLIC_UTIL_HTTP_URL_FETCHER_H_ diff --git a/chromium/headless/public/util/in_memory_protocol_handler.cc b/chromium/headless/public/util/in_memory_protocol_handler.cc new file mode 100644 index 00000000000..27c076551db --- /dev/null +++ b/chromium/headless/public/util/in_memory_protocol_handler.cc @@ -0,0 +1,37 @@ +// 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 "headless/public/util/in_memory_protocol_handler.h" + +#include "headless/public/util/in_memory_request_job.h" + +namespace headless { + +InMemoryProtocolHandler::InMemoryProtocolHandler() {} +InMemoryProtocolHandler::~InMemoryProtocolHandler() {} + +net::URLRequestJob* InMemoryProtocolHandler::MaybeCreateJob( + net::URLRequest* request, + net::NetworkDelegate* network_delegate) const { + return new InMemoryRequestJob(request, network_delegate, + GetResponse(request->url().GetContent())); +} + +void InMemoryProtocolHandler::InsertResponse(const std::string& request_path, + const Response& response) { + // GURL::GetContent excludes the scheme and : but includes the // prefix. As a + // convenience we add it here. + response_map_["//" + request_path] = response; +} + +const InMemoryProtocolHandler::Response* InMemoryProtocolHandler::GetResponse( + const std::string& request_path) const { + std::map<std::string, Response>::const_iterator find_it = + response_map_.find(request_path); + if (find_it == response_map_.end()) + return nullptr; + return &find_it->second; +} + +} // namespace headless diff --git a/chromium/headless/public/util/in_memory_protocol_handler.h b/chromium/headless/public/util/in_memory_protocol_handler.h new file mode 100644 index 00000000000..1d69d46a9b2 --- /dev/null +++ b/chromium/headless/public/util/in_memory_protocol_handler.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 HEADLESS_PUBLIC_UTIL_IN_MEMORY_PROTOCOL_HANDLER_H_ +#define HEADLESS_PUBLIC_UTIL_IN_MEMORY_PROTOCOL_HANDLER_H_ + +#include <map> + +#include "net/url_request/url_request_job_factory.h" + +namespace headless { + +class InMemoryProtocolHandler + : public net::URLRequestJobFactory::ProtocolHandler { + public: + InMemoryProtocolHandler(); + ~InMemoryProtocolHandler() override; + + net::URLRequestJob* MaybeCreateJob( + net::URLRequest* request, + net::NetworkDelegate* network_delegate) const override; + + struct Response { + Response() {} + Response(const std::string& data, const std::string& mime_type) + : data(data), mime_type(mime_type) {} + + std::string data; + std::string mime_type; + }; + + void InsertResponse(const std::string& request_path, + const Response& response); + + private: + const Response* GetResponse(const std::string& request_path) const; + + std::map<std::string, Response> response_map_; + + DISALLOW_COPY_AND_ASSIGN(InMemoryProtocolHandler); +}; + +} // namespace headless + +#endif // HEADLESS_PUBLIC_UTIL_IN_MEMORY_PROTOCOL_HANDLER_H_ diff --git a/chromium/headless/public/util/in_memory_request_job.cc b/chromium/headless/public/util/in_memory_request_job.cc new file mode 100644 index 00000000000..5fa84151980 --- /dev/null +++ b/chromium/headless/public/util/in_memory_request_job.cc @@ -0,0 +1,65 @@ +// 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 "headless/public/util/in_memory_request_job.h" + +#include "base/threading/thread_task_runner_handle.h" +#include "headless/public/util/in_memory_protocol_handler.h" +#include "net/base/io_buffer.h" +#include "net/base/net_errors.h" +#include "net/url_request/url_request_status.h" + +namespace headless { + +InMemoryRequestJob::InMemoryRequestJob( + net::URLRequest* request, + net::NetworkDelegate* network_delegate, + const InMemoryProtocolHandler::Response* response) + : net::URLRequestJob(request, network_delegate), + response_(response), + data_offset_(0), + weak_factory_(this) {} + +InMemoryRequestJob::~InMemoryRequestJob() {} + +void InMemoryRequestJob::Start() { + // Start reading asynchronously so that all error reporting and data + // callbacks happen as they would for network requests. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(&InMemoryRequestJob::StartAsync, weak_factory_.GetWeakPtr())); +} + +void InMemoryRequestJob::StartAsync() { + if (!response_) { + NotifyStartError(net::URLRequestStatus(net::URLRequestStatus::FAILED, + net::ERR_FILE_NOT_FOUND)); + return; + } + NotifyHeadersComplete(); +} + +void InMemoryRequestJob::Kill() { + weak_factory_.InvalidateWeakPtrs(); + URLRequestJob::Kill(); +} + +int InMemoryRequestJob::ReadRawData(net::IOBuffer* buf, int buf_size) { + DCHECK(response_); + DCHECK_LE(data_offset_, response_->data.size()); + int remaining = + base::checked_cast<int>(response_->data.size() - data_offset_); + if (buf_size > remaining) + buf_size = remaining; + memcpy(buf->data(), response_->data.data() + data_offset_, buf_size); + data_offset_ += buf_size; + return buf_size; +} + +bool InMemoryRequestJob::GetMimeType(std::string* mime_type) const { + *mime_type = response_->mime_type; + return true; +} + +} // namespace headless diff --git a/chromium/headless/public/util/in_memory_request_job.h b/chromium/headless/public/util/in_memory_request_job.h new file mode 100644 index 00000000000..1526c9a470c --- /dev/null +++ b/chromium/headless/public/util/in_memory_request_job.h @@ -0,0 +1,45 @@ +// 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 HEADLESS_PUBLIC_UTIL_IN_MEMORY_URL_REQUEST_JOB_H_ +#define HEADLESS_PUBLIC_UTIL_IN_MEMORY_URL_REQUEST_JOB_H_ + +#include "base/memory/weak_ptr.h" +#include "headless/public/util/in_memory_protocol_handler.h" +#include "net/url_request/url_request_job.h" + +namespace net { +class StringIOBuffer; +class DrainableIOBuffer; +} + +namespace headless { +class InMemoryRequestJob : public net::URLRequestJob { + public: + InMemoryRequestJob(net::URLRequest* request, + net::NetworkDelegate* network_delegate, + const InMemoryProtocolHandler::Response* response); + + // net::URLRequestJob implementation: + void Start() override; + void Kill() override; + int ReadRawData(net::IOBuffer* buf, int buf_size) override; + bool GetMimeType(std::string* mime_type) const override; + + private: + ~InMemoryRequestJob() override; + + void StartAsync(); + + const InMemoryProtocolHandler::Response* response_; // NOT OWNED + size_t data_offset_; + + base::WeakPtrFactory<InMemoryRequestJob> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(InMemoryRequestJob); +}; + +} // namespace headless + +#endif // HEADLESS_PUBLIC_UTIL_IN_MEMORY_URL_REQUEST_JOB_H_ diff --git a/chromium/headless/public/util/managed_dispatch_url_request_job.cc b/chromium/headless/public/util/managed_dispatch_url_request_job.cc new file mode 100644 index 00000000000..2f79373af0d --- /dev/null +++ b/chromium/headless/public/util/managed_dispatch_url_request_job.cc @@ -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. + +#include "headless/public/util/managed_dispatch_url_request_job.h" + +#include "headless/public/util/url_request_dispatcher.h" + +namespace headless { + +ManagedDispatchURLRequestJob::ManagedDispatchURLRequestJob( + net::URLRequest* request, + net::NetworkDelegate* network_delegate, + URLRequestDispatcher* url_request_dispatcher) + : net::URLRequestJob(request, network_delegate), + url_request_dispatcher_(url_request_dispatcher) { + url_request_dispatcher_->JobCreated(this); +} + +ManagedDispatchURLRequestJob::~ManagedDispatchURLRequestJob() { + // We can be fairly certain the renderer has finished by the time the job gets + // deleted. + url_request_dispatcher_->JobDeleted(this); +} + +void ManagedDispatchURLRequestJob::Kill() { + url_request_dispatcher_->JobKilled(this); + URLRequestJob::Kill(); +} + +void ManagedDispatchURLRequestJob::DispatchStartError(net::Error error) { + url_request_dispatcher_->JobFailed(this, error); +} + +void ManagedDispatchURLRequestJob::DispatchHeadersComplete() { + url_request_dispatcher_->DataReady(this); +} + +void ManagedDispatchURLRequestJob::OnHeadersComplete() { + NotifyHeadersComplete(); +} + +void ManagedDispatchURLRequestJob::OnStartError(net::Error error) { + NotifyStartError(net::URLRequestStatus(net::URLRequestStatus::FAILED, error)); +} + +} // namespace headless diff --git a/chromium/headless/public/util/managed_dispatch_url_request_job.h b/chromium/headless/public/util/managed_dispatch_url_request_job.h new file mode 100644 index 00000000000..3903d2a96a0 --- /dev/null +++ b/chromium/headless/public/util/managed_dispatch_url_request_job.h @@ -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. + +#ifndef HEADLESS_PUBLIC_UTIL_MANAGED_DISPATCH_URL_REQUEST_JOB_H_ +#define HEADLESS_PUBLIC_UTIL_MANAGED_DISPATCH_URL_REQUEST_JOB_H_ + +#include "base/macros.h" +#include "net/base/net_errors.h" +#include "net/url_request/url_request.h" +#include "net/url_request/url_request_job.h" + +namespace headless { +class URLRequestDispatcher; + +// A ManagedDispatchURLRequestJob exists to allow a URLRequestDispatcher control +// the order in which a set of fetches complete. Typically this is done to make +// fetching deterministic. NOTE URLRequestDispatcher sub classes use +// OnHeadersComplete and OnStartError to drive the URLRequestJob. +class ManagedDispatchURLRequestJob : public net::URLRequestJob { + public: + ManagedDispatchURLRequestJob(net::URLRequest* request, + net::NetworkDelegate* network_delegate, + URLRequestDispatcher* url_request_dispatcher); + + ~ManagedDispatchURLRequestJob() override; + + // Tells the net::URLRequestJob that the data has been fetched and is ready to + // be consumed. + // Virtual for FakeManagedDispatchURLRequestJob. + virtual void OnHeadersComplete(); + + // Tells the net::URLRequestJob that the fetch failed. + // Virtual for FakeManagedDispatchURLRequestJob. + virtual void OnStartError(net::Error error); + + protected: + // net::URLRequestJob implementation: + void Kill() override; + + // Tell the dispatcher the request failed. + virtual void DispatchStartError(net::Error error); + + // Tell the dispatcher the data is ready for the net stack to consume. + virtual void DispatchHeadersComplete(); + + URLRequestDispatcher* url_request_dispatcher_; // Not owned. + + private: + // Derived classes should not use NotifyHeadersComplete or NotifyStartError + // directly. Instead they should use DispatchHeadersComplete or + // DispatchStartError. + using URLRequestJob::NotifyHeadersComplete; + using URLRequestJob::NotifyStartError; + + DISALLOW_COPY_AND_ASSIGN(ManagedDispatchURLRequestJob); +}; + +} // namespace headless + +#endif // HEADLESS_PUBLIC_UTIL_MANAGED_DISPATCH_URL_REQUEST_JOB_H_ diff --git a/chromium/headless/public/util/testing/fake_managed_dispatch_url_request_job.cc b/chromium/headless/public/util/testing/fake_managed_dispatch_url_request_job.cc new file mode 100644 index 00000000000..24191d3f026 --- /dev/null +++ b/chromium/headless/public/util/testing/fake_managed_dispatch_url_request_job.cc @@ -0,0 +1,17 @@ +// 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 "headless/public/util/testing/fake_managed_dispatch_url_request_job.h" + +namespace headless { + +void FakeManagedDispatchURLRequestJob::OnHeadersComplete() { + notifications_->push_back(base::StringPrintf("id: %d OK", id_)); +} + +void FakeManagedDispatchURLRequestJob::OnStartError(net::Error error) { + notifications_->push_back(base::StringPrintf("id: %d err: %d", id_, error)); +} + +} // namespace headless diff --git a/chromium/headless/public/util/testing/fake_managed_dispatch_url_request_job.h b/chromium/headless/public/util/testing/fake_managed_dispatch_url_request_job.h new file mode 100644 index 00000000000..cb0dabdf5b1 --- /dev/null +++ b/chromium/headless/public/util/testing/fake_managed_dispatch_url_request_job.h @@ -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. + +#ifndef HEADLESS_PUBLIC_UTIL_TESTING_FAKE_MANAGED_DISPATCH_URL_REQUEST_JOB_H_ +#define HEADLESS_PUBLIC_UTIL_TESTING_FAKE_MANAGED_DISPATCH_URL_REQUEST_JOB_H_ + +#include <string> +#include <vector> + +#include "base/macros.h" +#include "base/strings/stringprintf.h" +#include "headless/public/util/managed_dispatch_url_request_job.h" +#include "net/base/net_errors.h" + +namespace headless { + +class URLRequestDispatcher; + +class FakeManagedDispatchURLRequestJob : public ManagedDispatchURLRequestJob { + public: + FakeManagedDispatchURLRequestJob(URLRequestDispatcher* url_request_dispatcher, + int id, + std::vector<std::string>* notifications) + : ManagedDispatchURLRequestJob(nullptr, nullptr, url_request_dispatcher), + id_(id), + notifications_(notifications) {} + + ~FakeManagedDispatchURLRequestJob() override {} + + using ManagedDispatchURLRequestJob::DispatchHeadersComplete; + using ManagedDispatchURLRequestJob::DispatchStartError; + + void Start() override {} + + void OnHeadersComplete() override; + + void OnStartError(net::Error error) override; + + private: + int id_; + std::vector<std::string>* notifications_; // NOT OWNED + + DISALLOW_COPY_AND_ASSIGN(FakeManagedDispatchURLRequestJob); +}; + +} // namespace headless + +#endif // HEADLESS_PUBLIC_UTIL_TESTING_FAKE_MANAGED_DISPATCH_URL_REQUEST_JOB_H_ diff --git a/chromium/headless/public/util/testing/generic_url_request_mocks.cc b/chromium/headless/public/util/testing/generic_url_request_mocks.cc new file mode 100644 index 00000000000..08ff33d4b07 --- /dev/null +++ b/chromium/headless/public/util/testing/generic_url_request_mocks.cc @@ -0,0 +1,172 @@ +// 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 "headless/public/util/testing/generic_url_request_mocks.h" + +#include "base/logging.h" + +namespace net { +class URLRequestJob; +} // namespace net + +namespace headless { + +// MockGenericURLRequestJobDelegate +MockGenericURLRequestJobDelegate::MockGenericURLRequestJobDelegate() + : should_block_(false) {} +MockGenericURLRequestJobDelegate::~MockGenericURLRequestJobDelegate() {} + +bool MockGenericURLRequestJobDelegate::BlockOrRewriteRequest( + const GURL& url, + const std::string& method, + const std::string& referrer, + GenericURLRequestJob::RewriteCallback callback) { + if (should_block_) + callback(GenericURLRequestJob::RewriteResult::kDeny, GURL(), method); + return should_block_; +} + +const GenericURLRequestJob::HttpResponse* +MockGenericURLRequestJobDelegate::MaybeMatchResource( + const GURL& url, + const std::string& method, + const net::HttpRequestHeaders& request_headers) { + return nullptr; +} + +void MockGenericURLRequestJobDelegate::OnResourceLoadComplete( + const GURL& final_url, + const std::string& mime_type, + int http_response_code) {} + +// MockCookieStore +MockCookieStore::MockCookieStore() {} +MockCookieStore::~MockCookieStore() {} + +void MockCookieStore::SetCookieWithOptionsAsync( + const GURL& url, + const std::string& cookie_line, + const net::CookieOptions& options, + const SetCookiesCallback& callback) { + CHECK(false); +} + +void MockCookieStore::SetCookieWithDetailsAsync( + const GURL& url, + const std::string& name, + const std::string& value, + const std::string& domain, + const std::string& path, + base::Time creation_time, + base::Time expiration_time, + base::Time last_access_time, + bool secure, + bool http_only, + net::CookieSameSite same_site, + bool enforce_strict_secure, + net::CookiePriority priority, + const SetCookiesCallback& callback) { + CHECK(false); +} + +void MockCookieStore::GetCookiesWithOptionsAsync( + const GURL& url, + const net::CookieOptions& options, + const GetCookiesCallback& callback) { + CHECK(false); +} + +void MockCookieStore::GetCookieListWithOptionsAsync( + const GURL& url, + const net::CookieOptions& options, + const GetCookieListCallback& callback) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(&MockCookieStore::SendCookies, + base::Unretained(this), url, options, callback)); +} + +void MockCookieStore::GetAllCookiesAsync( + const GetCookieListCallback& callback) { + CHECK(false); +} + +void MockCookieStore::DeleteCookieAsync(const GURL& url, + const std::string& cookie_name, + const base::Closure& callback) { + CHECK(false); +} + +void MockCookieStore::DeleteCanonicalCookieAsync( + const net::CanonicalCookie& cookie, + const DeleteCallback& callback) { + CHECK(false); +} + +void MockCookieStore::DeleteAllCreatedBetweenAsync( + const base::Time& delete_begin, + const base::Time& delete_end, + const DeleteCallback& callback) { + CHECK(false); +} + +void MockCookieStore::DeleteAllCreatedBetweenWithPredicateAsync( + const base::Time& delete_begin, + const base::Time& delete_end, + const CookiePredicate& predicate, + const DeleteCallback& callback) { + CHECK(false); +} + +void MockCookieStore::DeleteSessionCookiesAsync(const DeleteCallback&) { + CHECK(false); +} + +void MockCookieStore::FlushStore(const base::Closure& callback) { + CHECK(false); +} + +void MockCookieStore::SetForceKeepSessionState() { + CHECK(false); +} + +std::unique_ptr<net::CookieStore::CookieChangedSubscription> +MockCookieStore::AddCallbackForCookie(const GURL& url, + const std::string& name, + const CookieChangedCallback& callback) { + CHECK(false); + return nullptr; +} + +bool MockCookieStore::IsEphemeral() { + CHECK(false); + return true; +} + +void MockCookieStore::SendCookies(const GURL& url, + const net::CookieOptions& options, + const GetCookieListCallback& callback) { + net::CookieList result; + for (const auto& cookie : cookies_) { + if (cookie.IncludeForRequestURL(url, options)) + result.push_back(cookie); + } + callback.Run(result); +} + +// MockURLRequestDelegate +MockURLRequestDelegate::MockURLRequestDelegate() {} +MockURLRequestDelegate::~MockURLRequestDelegate() {} + +void MockURLRequestDelegate::OnResponseStarted(net::URLRequest* request) {} +void MockURLRequestDelegate::OnReadCompleted(net::URLRequest* request, + int bytes_read) {} +const std::string& MockURLRequestDelegate::response_data() const { + return response_data_; +} + +const net::IOBufferWithSize* MockURLRequestDelegate::metadata() const { + return nullptr; +} + +} // namespace headless diff --git a/chromium/headless/public/util/testing/generic_url_request_mocks.h b/chromium/headless/public/util/testing/generic_url_request_mocks.h new file mode 100644 index 00000000000..7dc683fce64 --- /dev/null +++ b/chromium/headless/public/util/testing/generic_url_request_mocks.h @@ -0,0 +1,155 @@ +// 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 HEADLESS_PUBLIC_UTIL_TESTING_GENERIC_URL_REQUEST_MOCKS_H_ +#define HEADLESS_PUBLIC_UTIL_TESTING_GENERIC_URL_REQUEST_MOCKS_H_ + +#include <memory> +#include <string> + +#include "base/macros.h" +#include "headless/public/util/generic_url_request_job.h" +#include "headless/public/util/testing/generic_url_request_mocks.h" +#include "net/cookies/cookie_store.h" +#include "net/url_request/url_request.h" +#include "net/url_request/url_request_context.h" +#include "net/url_request/url_request_job_factory.h" + +namespace net { +class URLRequestJob; +} // namespace net + +namespace htmlrender_webkit_headless_proto { +class Resource; +} // htmlrender_webkit_headless_proto net + +namespace headless { + +class MockGenericURLRequestJobDelegate : public GenericURLRequestJob::Delegate { + public: + MockGenericURLRequestJobDelegate(); + ~MockGenericURLRequestJobDelegate() override; + + bool BlockOrRewriteRequest( + const GURL& url, + const std::string& method, + const std::string& referrer, + GenericURLRequestJob::RewriteCallback callback) override; + + const GenericURLRequestJob::HttpResponse* MaybeMatchResource( + const GURL& url, + const std::string& method, + const net::HttpRequestHeaders& request_headers) override; + + void OnResourceLoadComplete(const GURL& final_url, + const std::string& mime_type, + int http_response_code) override; + + void SetShouldBlock(bool should_block) { should_block_ = should_block; } + + private: + bool should_block_; + + DISALLOW_COPY_AND_ASSIGN(MockGenericURLRequestJobDelegate); +}; + +// TODO(alexclarke): We may be able to replace this with the CookieMonster. +class MockCookieStore : public net::CookieStore { + public: + MockCookieStore(); + ~MockCookieStore() override; + + // net::CookieStore implementation: + void SetCookieWithOptionsAsync(const GURL& url, + const std::string& cookie_line, + const net::CookieOptions& options, + const SetCookiesCallback& callback) override; + + void SetCookieWithDetailsAsync(const GURL& url, + const std::string& name, + const std::string& value, + const std::string& domain, + const std::string& path, + base::Time creation_time, + base::Time expiration_time, + base::Time last_access_time, + bool secure, + bool http_only, + net::CookieSameSite same_site, + bool enforce_strict_secure, + net::CookiePriority priority, + const SetCookiesCallback& callback) override; + + void GetCookiesWithOptionsAsync(const GURL& url, + const net::CookieOptions& options, + const GetCookiesCallback& callback) override; + + void GetCookieListWithOptionsAsync( + const GURL& url, + const net::CookieOptions& options, + const GetCookieListCallback& callback) override; + + void GetAllCookiesAsync(const GetCookieListCallback& callback) override; + + void DeleteCookieAsync(const GURL& url, + const std::string& cookie_name, + const base::Closure& callback) override; + + void DeleteCanonicalCookieAsync(const net::CanonicalCookie& cookie, + const DeleteCallback& callback) override; + + void DeleteAllCreatedBetweenAsync(const base::Time& delete_begin, + const base::Time& delete_end, + const DeleteCallback& callback) override; + + void DeleteAllCreatedBetweenWithPredicateAsync( + const base::Time& delete_begin, + const base::Time& delete_end, + const CookiePredicate& predicate, + const DeleteCallback& callback) override; + + void DeleteSessionCookiesAsync(const DeleteCallback&) override; + + void FlushStore(const base::Closure& callback) override; + + void SetForceKeepSessionState() override; + + std::unique_ptr<CookieChangedSubscription> AddCallbackForCookie( + const GURL& url, + const std::string& name, + const CookieChangedCallback& callback) override; + + bool IsEphemeral() override; + + net::CookieList* cookies() { return &cookies_; } + + private: + void SendCookies(const GURL& url, + const net::CookieOptions& options, + const GetCookieListCallback& callback); + + net::CookieList cookies_; + + DISALLOW_COPY_AND_ASSIGN(MockCookieStore); +}; + +class MockURLRequestDelegate : public net::URLRequest::Delegate { + public: + MockURLRequestDelegate(); + ~MockURLRequestDelegate() override; + + void OnResponseStarted(net::URLRequest* request) override; + void OnReadCompleted(net::URLRequest* request, int bytes_read) override; + const std::string& response_data() const; + const net::IOBufferWithSize* metadata() const; + + private: + std::string response_data_; + + DISALLOW_COPY_AND_ASSIGN(MockURLRequestDelegate); +}; + +} // namespace headless + +#endif // HEADLESS_PUBLIC_UTIL_TESTING_GENERIC_URL_REQUEST_MOCKS_H_ diff --git a/chromium/headless/public/util/url_fetcher.cc b/chromium/headless/public/util/url_fetcher.cc new file mode 100644 index 00000000000..8d8d33fc239 --- /dev/null +++ b/chromium/headless/public/util/url_fetcher.cc @@ -0,0 +1,40 @@ +// 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 "headless/public/util/url_fetcher.h" + +#include <utility> + +#include "base/logging.h" +#include "net/http/http_response_headers.h" +#include "net/http/http_util.h" + +namespace headless { + +void URLFetcher::ResultListener::OnFetchCompleteExtractHeaders( + const GURL& final_url, + int http_response_code, + const char* response_data, + size_t response_data_size) { + size_t read_offset = 0; + int header_size = + net::HttpUtil::LocateEndOfHeaders(response_data, response_data_size); + scoped_refptr<net::HttpResponseHeaders> response_headers; + + if (header_size == -1) { + LOG(WARNING) << "Can't find headers in result for url: " << final_url; + response_headers = new net::HttpResponseHeaders(""); + } else { + response_headers = new net::HttpResponseHeaders( + net::HttpUtil::AssembleRawHeaders(response_data, header_size)); + read_offset = header_size; + } + + CHECK_LE(read_offset, response_data_size); + OnFetchComplete(final_url, http_response_code, std::move(response_headers), + response_data + read_offset, + response_data_size - read_offset); +} + +} // namespace headless diff --git a/chromium/headless/public/util/url_fetcher.h b/chromium/headless/public/util/url_fetcher.h new file mode 100644 index 00000000000..ac4bbc51426 --- /dev/null +++ b/chromium/headless/public/util/url_fetcher.h @@ -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. + +#ifndef HEADLESS_PUBLIC_UTIL_URL_FETCHER_H_ +#define HEADLESS_PUBLIC_UTIL_URL_FETCHER_H_ + +#include <stddef.h> +#include <string> +#include <vector> + +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "net/base/net_errors.h" +#include "url/gurl.h" + +namespace net { +class HttpRequestHeaders; +class HttpResponseHeaders; +} // namespace net + +namespace headless { + +// An interface for fetching URLs. Note these are only intended to be used once. +class URLFetcher { + public: + URLFetcher() {} + virtual ~URLFetcher() {} + + // Interface for reporting the fetch result. + class ResultListener { + public: + ResultListener() {} + + // Informs the listener that the fetch failed. + virtual void OnFetchStartError(net::Error error) = 0; + + // Informs the listener that the fetch succeeded and returns the HTTP + // headers and the body. NOTE |body| must be owned by the caller and remain + // valid until the fetcher is destroyed, + virtual void OnFetchComplete( + const GURL& final_url, + int http_response_code, + scoped_refptr<net::HttpResponseHeaders> response_headers, + const char* body, + size_t body_size) = 0; + + // Helper function which extracts the headers from |response_data| and calls + // OnFetchComplete. + void OnFetchCompleteExtractHeaders(const GURL& final_url, + int http_response_code, + const char* response_data, + size_t response_data_size); + + protected: + virtual ~ResultListener() {} + + private: + DISALLOW_COPY_AND_ASSIGN(ResultListener); + }; + + // Instructs the sub-class to fetch the resource. + virtual void StartFetch(const GURL& rewritten_url, + const std::string& method, + const net::HttpRequestHeaders& request_headers, + ResultListener* result_listener) = 0; + + private: + DISALLOW_COPY_AND_ASSIGN(URLFetcher); +}; + +} // namespace headless + +#endif // HEADLESS_PUBLIC_UTIL_URL_FETCHER_H_ diff --git a/chromium/headless/public/util/url_request_dispatcher.h b/chromium/headless/public/util/url_request_dispatcher.h new file mode 100644 index 00000000000..1f34509a3a5 --- /dev/null +++ b/chromium/headless/public/util/url_request_dispatcher.h @@ -0,0 +1,45 @@ +// 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 HEADLESS_PUBLIC_UTIL_URL_REQUEST_DISPATCHER_H_ +#define HEADLESS_PUBLIC_UTIL_URL_REQUEST_DISPATCHER_H_ + +#include "base/macros.h" +#include "net/base/net_errors.h" + +namespace headless { +class ManagedDispatchURLRequestJob; + +// Interface to abstract and potentially reorder (for determinism) calls to +// ManagedDispatchUrlRequestJob::OnHeadersComplete and +// ManagedDispatchUrlRequestJob::NotifyStartError. +class URLRequestDispatcher { + public: + URLRequestDispatcher() {} + virtual ~URLRequestDispatcher() {} + + // Tells us a URLRequestJob was created. Can be called from any thread. + virtual void JobCreated(ManagedDispatchURLRequestJob* job) = 0; + + // Tells us a URLRequestJob got killed. Can be called from any thread. + virtual void JobKilled(ManagedDispatchURLRequestJob* job) = 0; + + // Tells us a URLRequestJob failed. Can be called from any thread. + virtual void JobFailed(ManagedDispatchURLRequestJob* job, + net::Error error) = 0; + + // Tells us a URLRequestJob has fetched the data and is ready for the net + // stack to consume it. Can be called from any thread. + virtual void DataReady(ManagedDispatchURLRequestJob* job) = 0; + + // Tells us the job has finished. Can be called from any thread. + virtual void JobDeleted(ManagedDispatchURLRequestJob* job) = 0; + + private: + DISALLOW_COPY_AND_ASSIGN(URLRequestDispatcher); +}; + +} // namespace headless + +#endif // HEADLESS_PUBLIC_UTIL_URL_REQUEST_DISPATCHER_H_ diff --git a/chromium/headless/public/util/user_agent.cc b/chromium/headless/public/util/user_agent.cc new file mode 100644 index 00000000000..13ffe9fb62a --- /dev/null +++ b/chromium/headless/public/util/user_agent.cc @@ -0,0 +1,20 @@ +// 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 "headless/public/util/user_agent.h" + +#include "content/public/common/user_agent.h" + +namespace headless { + +std::string BuildUserAgentFromProduct(const std::string& product) { + return content::BuildUserAgentFromProduct(product); +} + +std::string BuildUserAgentFromOSAndProduct(const std::string& os_info, + const std::string& product) { + return content::BuildUserAgentFromOSAndProduct(os_info, product); +} + +} // namespace headless diff --git a/chromium/headless/public/util/user_agent.h b/chromium/headless/public/util/user_agent.h new file mode 100644 index 00000000000..04f7f8636c8 --- /dev/null +++ b/chromium/headless/public/util/user_agent.h @@ -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. + +#ifndef HEADLESS_PUBLIC_UTIL_USER_AGENT_H_ +#define HEADLESS_PUBLIC_UTIL_USER_AGENT_H_ + +#include <string> + +namespace headless { + +std::string BuildUserAgentFromProduct(const std::string& product); +std::string BuildUserAgentFromOSAndProduct(const std::string& os_info, + const std::string& product); + +} // namespace headless + +#endif // HEADLESS_PUBLIC_UTIL_USER_AGENT_H_ |