| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Implements the Chrome Extensions Tab Capture API. |
| |
| #include "chrome/browser/extensions/api/tab_capture/tab_capture_api.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <set> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/command_line.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/values.h" |
| #include "chrome/browser/extensions/api/tab_capture/offscreen_tabs_owner.h" |
| #include "chrome/browser/extensions/api/tab_capture/tab_capture_registry.h" |
| #include "chrome/browser/extensions/extension_tab_util.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/sessions/session_tab_helper.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_list.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "content/public/browser/desktop_media_id.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/origin_util.h" |
| #include "extensions/common/features/feature.h" |
| #include "extensions/common/features/feature_provider.h" |
| #include "extensions/common/features/simple_feature.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "extensions/common/switches.h" |
| |
| using content::DesktopMediaID; |
| using content::WebContentsMediaCaptureId; |
| using extensions::api::tab_capture::MediaStreamConstraint; |
| |
| namespace TabCapture = extensions::api::tab_capture; |
| namespace GetCapturedTabs = TabCapture::GetCapturedTabs; |
| |
| namespace extensions { |
| namespace { |
| |
| const char kCapturingSameTab[] = "Cannot capture a tab with an active stream."; |
| const char kFindingTabError[] = "Error finding tab to capture."; |
| const char kNoAudioOrVideo[] = "Capture failed. No audio or video requested."; |
| const char kGrantError[] = |
| "Extension has not been invoked for the current page (see activeTab " |
| "permission). Chrome pages cannot be captured."; |
| |
| const char kNotWhitelistedForOffscreenTabApi[] = |
| "Extension is not whitelisted for use of the unstable, in-development " |
| "chrome.tabCapture.captureOffscreenTab API."; |
| const char kInvalidStartUrl[] = |
| "Invalid/Missing/Malformatted starting URL for off-screen tab."; |
| const char kTooManyOffscreenTabs[] = |
| "Extension has already started too many off-screen tabs."; |
| const char kCapturingSameOffscreenTab[] = |
| "Cannot capture the same off-screen tab more than once."; |
| |
| const char kInvalidOriginError[] = "Caller tab.url is not a valid URL."; |
| const char kInvalidTabIdError[] = "Invalid tab specified."; |
| const char kTabUrlNotSecure[] = |
| "URL scheme for the specified tab is not secure."; |
| |
| // Keys/values passed to renderer-side JS bindings. |
| const char kMediaStreamSource[] = "chromeMediaSource"; |
| const char kMediaStreamSourceId[] = "chromeMediaSourceId"; |
| const char kMediaStreamSourceTab[] = "tab"; |
| |
| // Tab Capture-specific video constraint to enable automatic resolution/rate |
| // throttling mode in the capture pipeline. |
| const char kEnableAutoThrottlingKey[] = "enableAutoThrottling"; |
| |
| bool OptionsSpecifyAudioOrVideo(const TabCapture::CaptureOptions& options) { |
| return (options.audio && *options.audio) || (options.video && *options.video); |
| } |
| |
| bool IsAcceptableOffscreenTabUrl(const GURL& url) { |
| return url.is_valid() && (url.SchemeIsHTTPOrHTTPS() || url.SchemeIs("data")); |
| } |
| |
| // Removes all mandatory and optional constraint entries that start with the |
| // "goog" prefix. These are never needed and may cause the renderer-side |
| // getUserMedia() call to fail. http://crbug.com/579729 |
| // |
| // TODO(miu): Remove once tabCapture API is migrated to new constraints spec. |
| // http://crbug.com/579729 |
| void FilterDeprecatedGoogConstraints(TabCapture::CaptureOptions* options) { |
| const auto FilterGoogKeysFromDictionary = [](base::DictionaryValue* dict) { |
| std::vector<std::string> bad_keys; |
| base::DictionaryValue::Iterator it(*dict); |
| for (; !it.IsAtEnd(); it.Advance()) { |
| if (base::StartsWith(it.key(), "goog", base::CompareCase::SENSITIVE)) |
| bad_keys.push_back(it.key()); |
| } |
| for (const std::string& k : bad_keys) { |
| std::unique_ptr<base::Value> ignored; |
| dict->RemoveWithoutPathExpansion(k, &ignored); |
| } |
| }; |
| |
| if (options->audio_constraints) { |
| FilterGoogKeysFromDictionary( |
| &options->audio_constraints->mandatory.additional_properties); |
| if (options->audio_constraints->optional) { |
| FilterGoogKeysFromDictionary( |
| &options->audio_constraints->optional->additional_properties); |
| } |
| } |
| |
| if (options->video_constraints) { |
| FilterGoogKeysFromDictionary( |
| &options->video_constraints->mandatory.additional_properties); |
| if (options->video_constraints->optional) { |
| FilterGoogKeysFromDictionary( |
| &options->video_constraints->optional->additional_properties); |
| } |
| } |
| } |
| |
| bool GetAutoThrottlingFromOptions(TabCapture::CaptureOptions* options) { |
| bool enable_auto_throttling = false; |
| if (options && options->video && *options->video) { |
| if (options->video_constraints) { |
| // Check for the Tab Capture-specific video constraint for enabling |
| // automatic resolution/rate throttling mode in the capture pipeline. See |
| // implementation comments for content::WebContentsVideoCaptureDevice. |
| base::DictionaryValue& props = |
| options->video_constraints->mandatory.additional_properties; |
| if (!props.GetBooleanWithoutPathExpansion( |
| kEnableAutoThrottlingKey, &enable_auto_throttling)) { |
| enable_auto_throttling = false; |
| } |
| // Remove the key from the properties to avoid an "unrecognized |
| // constraint" error in the renderer. |
| props.RemoveWithoutPathExpansion(kEnableAutoThrottlingKey, nullptr); |
| } |
| } |
| |
| return enable_auto_throttling; |
| } |
| |
| DesktopMediaID BuildDesktopMediaID(content::WebContents* target_contents, |
| TabCapture::CaptureOptions* options) { |
| content::RenderFrameHost* const target_frame = |
| target_contents->GetMainFrame(); |
| DesktopMediaID source( |
| DesktopMediaID::TYPE_WEB_CONTENTS, DesktopMediaID::kNullId, |
| WebContentsMediaCaptureId(target_frame->GetProcess()->GetID(), |
| target_frame->GetRoutingID(), |
| GetAutoThrottlingFromOptions(options), false)); |
| return source; |
| } |
| |
| // Add Chrome-specific source identifiers to the MediaStreamConstraints objects |
| // in |options| to provide references to the |target_contents| to be captured. |
| void AddMediaStreamSourceConstraints(content::WebContents* target_contents, |
| TabCapture::CaptureOptions* options, |
| const std::string& device_id) { |
| DCHECK(options); |
| DCHECK(target_contents); |
| |
| MediaStreamConstraint* constraints_to_modify[2] = {nullptr, nullptr}; |
| |
| if (options->audio && *options->audio) { |
| if (!options->audio_constraints) |
| options->audio_constraints.reset(new MediaStreamConstraint); |
| constraints_to_modify[0] = options->audio_constraints.get(); |
| } |
| |
| if (options->video && *options->video) { |
| if (!options->video_constraints) |
| options->video_constraints.reset(new MediaStreamConstraint); |
| constraints_to_modify[1] = options->video_constraints.get(); |
| } |
| |
| // Append chrome specific tab constraints. |
| for (MediaStreamConstraint* msc : constraints_to_modify) { |
| if (!msc) |
| continue; |
| base::DictionaryValue* constraint = &msc->mandatory.additional_properties; |
| constraint->SetString(kMediaStreamSource, kMediaStreamSourceTab); |
| constraint->SetString(kMediaStreamSourceId, device_id); |
| } |
| } |
| |
| // Find the last-active browser that matches a profile this ExtensionFunction |
| // can access. We can't use FindLastActiveWithProfile() because we may want to |
| // include incognito profile browsers. |
| Browser* GetLastActiveBrowser(const Profile* profile, |
| const bool match_incognito_profile) { |
| BrowserList* browser_list = BrowserList::GetInstance(); |
| Browser* target_browser = nullptr; |
| for (auto iter = browser_list->begin_last_active(); |
| iter != browser_list->end_last_active(); ++iter) { |
| Profile* browser_profile = (*iter)->profile(); |
| if (browser_profile == profile || |
| (match_incognito_profile && |
| browser_profile->GetOriginalProfile() == profile)) { |
| target_browser = *iter; |
| break; |
| } |
| } |
| |
| return target_browser; |
| } |
| |
| } // namespace |
| |
| // Whitelisted extensions that do not check for a browser action grant because |
| // they provide API's. If there are additional extension ids that need |
| // whitelisting and are *not* the Media Router extension, add them to a new |
| // kWhitelist array. |
| const char* const kMediaRouterExtensionIds[] = { |
| "enhhojjnijigcajfphajepfemndkmdlo", // Dev |
| "pkedcjkdefgpdelpbcmbmeomcjbeemfm", // Stable |
| }; |
| |
| ExtensionFunction::ResponseAction TabCaptureCaptureFunction::Run() { |
| std::unique_ptr<api::tab_capture::Capture::Params> params = |
| TabCapture::Capture::Params::Create(*args_); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| Profile* profile = Profile::FromBrowserContext(browser_context()); |
| const bool match_incognito_profile = include_incognito_information(); |
| Browser* target_browser = |
| GetLastActiveBrowser(profile, match_incognito_profile); |
| if (!target_browser) |
| return RespondNow(Error(kFindingTabError)); |
| |
| content::WebContents* target_contents = |
| target_browser->tab_strip_model()->GetActiveWebContents(); |
| if (!target_contents) |
| return RespondNow(Error(kFindingTabError)); |
| |
| const std::string& extension_id = extension()->id(); |
| |
| // Make sure either we have been granted permission to capture through an |
| // extension icon click or our extension is whitelisted. |
| if (!extension()->permissions_data()->HasAPIPermissionForTab( |
| SessionTabHelper::IdForTab(target_contents).id(), |
| APIPermission::kTabCaptureForTab) && |
| base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| switches::kWhitelistedExtensionID) != extension_id && |
| !SimpleFeature::IsIdInArray(extension_id, kMediaRouterExtensionIds, |
| base::size(kMediaRouterExtensionIds))) { |
| return RespondNow(Error(kGrantError)); |
| } |
| |
| if (!OptionsSpecifyAudioOrVideo(params->options)) |
| return RespondNow(Error(kNoAudioOrVideo)); |
| |
| DesktopMediaID source = |
| BuildDesktopMediaID(target_contents, ¶ms->options); |
| content::WebContents* const extension_web_contents = GetSenderWebContents(); |
| EXTENSION_FUNCTION_VALIDATE(extension_web_contents); |
| TabCaptureRegistry* registry = TabCaptureRegistry::Get(browser_context()); |
| std::string device_id = registry->AddRequest( |
| target_contents, extension_id, false, extension()->url(), source, |
| extension()->name(), extension_web_contents); |
| if (device_id.empty()) { |
| // TODO(miu): Allow multiple consumers of single tab capture. |
| // http://crbug.com/535336 |
| return RespondNow(Error(kCapturingSameTab)); |
| } |
| FilterDeprecatedGoogConstraints(¶ms->options); |
| AddMediaStreamSourceConstraints(target_contents, ¶ms->options, device_id); |
| |
| // At this point, everything is set up in the browser process. It's now up to |
| // the custom JS bindings in the extension's render process to request a |
| // MediaStream using navigator.webkitGetUserMedia(). The result dictionary, |
| // passed to SetResult() here, contains the extra "hidden options" that will |
| // allow the Chrome platform implementation for getUserMedia() to start the |
| // virtual audio/video capture devices and set up all the data flows. The |
| // custom JS bindings can be found here: |
| // chrome/renderer/resources/extensions/tab_capture_custom_bindings.js |
| std::unique_ptr<base::DictionaryValue> result(new base::DictionaryValue()); |
| result->MergeDictionary(params->options.ToValue().get()); |
| return RespondNow(OneArgument(std::move(result))); |
| } |
| |
| ExtensionFunction::ResponseAction TabCaptureGetCapturedTabsFunction::Run() { |
| TabCaptureRegistry* registry = TabCaptureRegistry::Get(browser_context()); |
| std::unique_ptr<base::ListValue> list(new base::ListValue()); |
| if (registry) |
| registry->GetCapturedTabs(extension()->id(), list.get()); |
| return RespondNow(OneArgument(std::move(list))); |
| } |
| |
| ExtensionFunction::ResponseAction TabCaptureCaptureOffscreenTabFunction::Run() { |
| std::unique_ptr<TabCapture::CaptureOffscreenTab::Params> params = |
| TabCapture::CaptureOffscreenTab::Params::Create(*args_); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| // Make sure the extension is whitelisted for using this API, regardless of |
| // Chrome channel. |
| // |
| // TODO(miu): Use _api_features.json and extensions::Feature library instead. |
| // http://crbug.com/537732 |
| const bool is_whitelisted_extension = |
| base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| switches::kWhitelistedExtensionID) == extension()->id() || |
| SimpleFeature::IsIdInArray(extension()->id(), kMediaRouterExtensionIds, |
| base::size(kMediaRouterExtensionIds)); |
| if (!is_whitelisted_extension) |
| return RespondNow(Error(kNotWhitelistedForOffscreenTabApi)); |
| |
| const GURL start_url(params->start_url); |
| if (!IsAcceptableOffscreenTabUrl(start_url)) |
| return RespondNow(Error(kInvalidStartUrl)); |
| |
| if (!OptionsSpecifyAudioOrVideo(params->options)) |
| return RespondNow(Error(kNoAudioOrVideo)); |
| |
| content::WebContents* const extension_web_contents = GetSenderWebContents(); |
| EXTENSION_FUNCTION_VALIDATE(extension_web_contents); |
| OffscreenTab* const offscreen_tab = |
| OffscreenTabsOwner::Get(extension_web_contents)->OpenNewTab( |
| start_url, |
| DetermineInitialSize(params->options), |
| (is_whitelisted_extension && params->options.presentation_id) ? |
| *params->options.presentation_id : std::string()); |
| if (!offscreen_tab) |
| return RespondNow(Error(kTooManyOffscreenTabs)); |
| |
| content::WebContents* target_contents = offscreen_tab->web_contents(); |
| const std::string& extension_id = extension()->id(); |
| DesktopMediaID source = |
| BuildDesktopMediaID(target_contents, ¶ms->options); |
| TabCaptureRegistry* registry = TabCaptureRegistry::Get(browser_context()); |
| std::string device_id = registry->AddRequest( |
| target_contents, extension_id, true, extension()->url(), source, |
| extension()->name(), extension_web_contents); |
| if (device_id.empty()) { |
| // TODO(miu): Allow multiple consumers of single tab capture. |
| // http://crbug.com/535336 |
| return RespondNow(Error(kCapturingSameOffscreenTab)); |
| } |
| FilterDeprecatedGoogConstraints(¶ms->options); |
| AddMediaStreamSourceConstraints(target_contents, ¶ms->options, device_id); |
| |
| // At this point, everything is set up in the browser process. It's now up to |
| // the custom JS bindings in the extension's render process to complete the |
| // request. See the comment at end of TabCaptureCaptureFunction::RunSync() |
| // for more details. |
| return RespondNow(OneArgument(params->options.ToValue())); |
| } |
| |
| // static |
| gfx::Size TabCaptureCaptureOffscreenTabFunction::DetermineInitialSize( |
| const TabCapture::CaptureOptions& options) { |
| static const int kDefaultWidth = 1280; |
| static const int kDefaultHeight = 720; |
| |
| if (!options.video_constraints) |
| return gfx::Size(kDefaultWidth, kDefaultHeight); |
| |
| gfx::Size min_size; |
| int width = -1; |
| int height = -1; |
| const base::DictionaryValue& mandatory_properties = |
| options.video_constraints->mandatory.additional_properties; |
| if (mandatory_properties.GetInteger("maxWidth", &width) && width >= 0 && |
| mandatory_properties.GetInteger("maxHeight", &height) && height >= 0) { |
| return gfx::Size(width, height); |
| } |
| if (mandatory_properties.GetInteger("minWidth", &width) && width >= 0 && |
| mandatory_properties.GetInteger("minHeight", &height) && height >= 0) { |
| min_size.SetSize(width, height); |
| } |
| |
| // Use optional size constraints if no mandatory ones were provided. |
| if (options.video_constraints->optional) { |
| const base::DictionaryValue& optional_properties = |
| options.video_constraints->optional->additional_properties; |
| if (optional_properties.GetInteger("maxWidth", &width) && width >= 0 && |
| optional_properties.GetInteger("maxHeight", &height) && height >= 0) { |
| if (min_size.IsEmpty()) { |
| return gfx::Size(width, height); |
| } else { |
| return gfx::Size(std::max(width, min_size.width()), |
| std::max(height, min_size.height())); |
| } |
| } |
| if (min_size.IsEmpty() && |
| optional_properties.GetInteger("minWidth", &width) && width >= 0 && |
| optional_properties.GetInteger("minHeight", &height) && height >= 0) { |
| min_size.SetSize(width, height); |
| } |
| } |
| |
| // No maximum size was provided, so just return the default size bounded by |
| // the minimum size. |
| return gfx::Size(std::max(kDefaultWidth, min_size.width()), |
| std::max(kDefaultHeight, min_size.height())); |
| } |
| |
| ExtensionFunction::ResponseAction TabCaptureGetMediaStreamIdFunction::Run() { |
| std::unique_ptr<api::tab_capture::GetMediaStreamId::Params> params = |
| TabCapture::GetMediaStreamId::Params::Create(*args_); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| content::WebContents* target_contents = nullptr; |
| if (params->options && params->options->target_tab_id) { |
| if (!ExtensionTabUtil::GetTabById(*(params->options->target_tab_id), |
| browser_context(), true, nullptr, nullptr, |
| &target_contents, nullptr)) { |
| return RespondNow(Error(kInvalidTabIdError)); |
| } |
| } else { |
| Profile* profile = Profile::FromBrowserContext(browser_context()); |
| const bool match_incognito_profile = include_incognito_information(); |
| Browser* target_browser = |
| GetLastActiveBrowser(profile, match_incognito_profile); |
| if (!target_browser) |
| return RespondNow(Error(kFindingTabError)); |
| |
| target_contents = target_browser->tab_strip_model()->GetActiveWebContents(); |
| } |
| if (!target_contents) |
| return RespondNow(Error(kFindingTabError)); |
| |
| const std::string& extension_id = extension()->id(); |
| |
| // Make sure either we have been granted permission to capture through an |
| // extension icon click or our extension is whitelisted. |
| if (!extension()->permissions_data()->HasAPIPermissionForTab( |
| SessionTabHelper::IdForTab(target_contents).id(), |
| APIPermission::kTabCaptureForTab) && |
| base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| switches::kWhitelistedExtensionID) != extension_id) { |
| return RespondNow(Error(kGrantError)); |
| } |
| |
| // |consumer_contents| is the WebContents for which the stream is created. |
| content::WebContents* consumer_contents = nullptr; |
| std::string consumer_name; |
| GURL origin; |
| if (params->options && params->options->consumer_tab_id) { |
| if (!ExtensionTabUtil::GetTabById(*(params->options->consumer_tab_id), |
| browser_context(), true, nullptr, nullptr, |
| &consumer_contents, nullptr)) { |
| return RespondNow(Error(kInvalidTabIdError)); |
| } |
| |
| origin = consumer_contents->GetLastCommittedURL().GetOrigin(); |
| if (!origin.is_valid()) { |
| return RespondNow(Error(kInvalidOriginError)); |
| } |
| |
| if (!content::IsOriginSecure(origin)) { |
| return RespondNow(Error(kTabUrlNotSecure)); |
| } |
| |
| consumer_name = net::GetHostAndOptionalPort(origin); |
| } else { |
| origin = extension()->url(); |
| consumer_name = extension()->name(); |
| consumer_contents = GetSenderWebContents(); |
| } |
| EXTENSION_FUNCTION_VALIDATE(consumer_contents); |
| |
| DesktopMediaID source = BuildDesktopMediaID(target_contents, nullptr); |
| TabCaptureRegistry* registry = TabCaptureRegistry::Get(browser_context()); |
| std::string device_id = |
| registry->AddRequest(target_contents, extension_id, false, origin, source, |
| consumer_name, consumer_contents); |
| if (device_id.empty()) { |
| return RespondNow(Error(kCapturingSameTab)); |
| } |
| |
| return RespondNow(OneArgument(std::make_unique<base::Value>(device_id))); |
| } |
| |
| } // namespace extensions |