blob: 1ca877c8cb7364252e1c77c42d8cf0c72d9f6956 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/media/media_devices_util.h"
#include <utility>
#include <vector>
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/strings/string_tokenizer.h"
#include "content/browser/browser_main_loop.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/media/media_stream_manager.h"
#include "content/browser/renderer_host/render_frame_host_delegate.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "crypto/hmac.h"
#include "media/base/media_switches.h"
#include "net/cookies/site_for_cookies.h"
#include "url/origin.h"
namespace content {
using ::blink::mojom::MediaDeviceType;
using ::blink::mojom::MediaStreamType;
namespace {
std::string GetDefaultMediaDeviceIDOnUIThread(MediaDeviceType device_type,
int render_process_id,
int render_frame_id) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
RenderFrameHostImpl* frame_host =
RenderFrameHostImpl::FromID(render_process_id, render_frame_id);
if (!frame_host)
return std::string();
RenderFrameHostDelegate* delegate = frame_host->delegate();
if (!delegate)
return std::string();
MediaStreamType media_stream_type;
switch (device_type) {
case MediaDeviceType::MEDIA_AUDIO_INPUT:
media_stream_type = MediaStreamType::DEVICE_AUDIO_CAPTURE;
break;
case MediaDeviceType::MEDIA_VIDEO_INPUT:
media_stream_type = MediaStreamType::DEVICE_VIDEO_CAPTURE;
break;
default:
return std::string();
}
return delegate->GetDefaultMediaDeviceID(media_stream_type);
}
// This function is intended for testing purposes. It returns an empty string
// if no default device is supplied via the command line.
std::string GetDefaultMediaDeviceIDFromCommandLine(
MediaDeviceType device_type) {
DCHECK(base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kUseFakeDeviceForMediaStream));
const std::string option =
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kUseFakeDeviceForMediaStream);
// Optional comma delimited parameters to the command line can specify values
// for the default device IDs.
// Examples: "video-input-default-id=mycam, audio-input-default-id=mymic"
base::StringTokenizer option_tokenizer(option, ", ");
option_tokenizer.set_quote_chars("\"");
while (option_tokenizer.GetNext()) {
std::vector<base::StringPiece> param = base::SplitStringPiece(
option_tokenizer.token_piece(), "=", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
if (param.size() != 2u) {
DLOG(WARNING) << "Forgot a value '" << option << "'? Use name=value for "
<< switches::kUseFakeDeviceForMediaStream << ".";
return std::string();
}
if (device_type == MediaDeviceType::MEDIA_AUDIO_INPUT &&
param.front() == "audio-input-default-id") {
return std::string(param.back());
} else if (device_type == MediaDeviceType::MEDIA_VIDEO_INPUT &&
param.front() == "video-input-default-id") {
return std::string(param.back());
}
}
return std::string();
}
void GotSalt(const std::string& frame_salt,
const url::Origin& origin,
bool has_focus,
bool is_background,
absl::optional<ukm::SourceId> source_id,
MediaDeviceSaltAndOriginCallback callback,
bool are_persistent_device_ids_allowed,
const std::string& salt) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
std::string device_id_salt = salt;
if (!are_persistent_device_ids_allowed) {
device_id_salt += frame_salt;
}
// |group_id_salt| must be unique per document, but it must also change if
// cookies are cleared. Also, it must be different from |device_id_salt|,
// thus appending a constant.
std::string group_id_salt = base::StrCat({salt, frame_salt, "group_id"});
MediaDeviceSaltAndOrigin salt_and_origin(std::move(device_id_salt), origin,
std::move(group_id_salt), has_focus,
is_background, std::move(source_id));
std::move(callback).Run(salt_and_origin);
}
void FinalizeGetRawMediaDeviceIDForHMAC(
MediaDeviceType type,
const MediaDeviceSaltAndOrigin& salt_and_origin,
const std::string& source_id,
scoped_refptr<base::SequencedTaskRunner> task_runner,
base::OnceCallback<void(const absl::optional<std::string>&)> callback,
const MediaDeviceEnumeration& enumeration) {
for (const auto& device : enumeration[static_cast<size_t>(type)]) {
if (DoesRawMediaDeviceIDMatchHMAC(salt_and_origin, source_id,
device.device_id)) {
task_runner->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), device.device_id));
return;
}
}
task_runner->PostTask(FROM_HERE,
base::BindOnce(std::move(callback), absl::nullopt));
}
MediaDeviceType ConvertToMediaDeviceType(MediaStreamType stream_type) {
switch (stream_type) {
case MediaStreamType::DEVICE_AUDIO_CAPTURE:
return MediaDeviceType::MEDIA_AUDIO_INPUT;
case MediaStreamType::DEVICE_VIDEO_CAPTURE:
return MediaDeviceType::MEDIA_VIDEO_INPUT;
default:
NOTREACHED();
}
return MediaDeviceType::NUM_MEDIA_DEVICE_TYPES;
}
} // namespace
MediaDeviceSaltAndOrigin::MediaDeviceSaltAndOrigin(
std::string device_id_salt,
url::Origin origin,
std::string group_id_salt,
bool has_focus,
bool is_background,
absl::optional<ukm::SourceId> ukm_source_id)
: device_id_salt_(std::move(device_id_salt)),
group_id_salt_(std::move(group_id_salt)),
origin_(std::move(origin)),
ukm_source_id_(std::move(ukm_source_id)),
has_focus_(has_focus),
is_background_(is_background) {}
MediaDeviceSaltAndOrigin::MediaDeviceSaltAndOrigin(
const MediaDeviceSaltAndOrigin& other) = default;
MediaDeviceSaltAndOrigin& MediaDeviceSaltAndOrigin::operator=(
const MediaDeviceSaltAndOrigin& other) = default;
MediaDeviceSaltAndOrigin::MediaDeviceSaltAndOrigin(
MediaDeviceSaltAndOrigin&& other) = default;
MediaDeviceSaltAndOrigin& MediaDeviceSaltAndOrigin::operator=(
MediaDeviceSaltAndOrigin&& other) = default;
MediaDeviceSaltAndOrigin::~MediaDeviceSaltAndOrigin() = default;
MediaDeviceSaltAndOrigin MediaDeviceSaltAndOrigin::Empty() {
return MediaDeviceSaltAndOrigin(/*device_id_salt=*/std::string(),
/*origin=*/url::Origin());
}
void GetDefaultMediaDeviceID(MediaDeviceType device_type,
int render_process_id,
int render_frame_id,
DeviceIdCallback callback) {
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kUseFakeDeviceForMediaStream)) {
std::string command_line_default_device_id =
GetDefaultMediaDeviceIDFromCommandLine(device_type);
if (!command_line_default_device_id.empty()) {
std::move(callback).Run(command_line_default_device_id);
return;
}
}
GetUIThreadTaskRunner({})->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&GetDefaultMediaDeviceIDOnUIThread, device_type,
render_process_id, render_frame_id),
std::move(callback));
}
void GetMediaDeviceSaltAndOrigin(GlobalRenderFrameHostId render_frame_host_id,
MediaDeviceSaltAndOriginCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
RenderFrameHostImpl* frame_host =
RenderFrameHostImpl::FromID(render_frame_host_id);
if (!frame_host) {
std::move(callback).Run(MediaDeviceSaltAndOrigin::Empty());
return;
}
// Check that the frame is not in the prerendering state. Media playback is
// deferred while prerendering. So this check should pass and ensures the
// condition to call GetPageUkmSourceId below as the data collection policy
// disallows recording UKMs while prerendering.
CHECK(!frame_host->IsInLifecycleState(
RenderFrameHost::LifecycleState::kPrerendering));
net::SiteForCookies site_for_cookies = frame_host->ComputeSiteForCookies();
url::Origin origin = frame_host->GetLastCommittedOrigin();
bool has_focus = frame_host->GetView() && frame_host->GetView()->HasFocus();
absl::optional<ukm::SourceId> source_id = frame_host->GetPageUkmSourceId();
WebContents* web_contents = WebContents::FromRenderFrameHost(frame_host);
bool is_background =
web_contents && web_contents->GetDelegate() &&
web_contents->GetDelegate()->IsNeverComposited(web_contents);
std::string frame_salt = frame_host->GetMediaDeviceIDSaltBase();
GetContentClient()->browser()->GetMediaDeviceIDSalt(
frame_host, site_for_cookies, frame_host->GetStorageKey(),
base::BindOnce(&GotSalt, std::move(frame_salt), std::move(origin),
has_focus, is_background, std::move(source_id),
std::move(callback)));
}
blink::WebMediaDeviceInfo TranslateMediaDeviceInfo(
bool has_permission,
const MediaDeviceSaltAndOrigin& salt_and_origin,
const blink::WebMediaDeviceInfo& device_info) {
bool should_show_device_ids =
has_permission ||
!base::FeatureList::IsEnabled(features::kEnumerateDevicesHideDeviceIDs);
return blink::WebMediaDeviceInfo(
should_show_device_ids
? GetHMACForRawMediaDeviceID(salt_and_origin, device_info.device_id)
: std::string(),
has_permission ? device_info.label : std::string(),
should_show_device_ids && !device_info.group_id.empty()
? GetHMACForRawMediaDeviceID(salt_and_origin, device_info.group_id,
/*use_group_salt=*/true)
: std::string(),
has_permission ? device_info.video_control_support
: media::VideoCaptureControlSupport(),
has_permission ? device_info.video_facing
: blink::mojom::FacingMode::NONE);
}
blink::WebMediaDeviceInfoArray TranslateMediaDeviceInfoArray(
bool has_permission,
const MediaDeviceSaltAndOrigin& salt_and_origin,
const blink::WebMediaDeviceInfoArray& device_infos) {
const bool should_hide_device_ids_with_no_permission =
base::FeatureList::IsEnabled(features::kEnumerateDevicesHideDeviceIDs);
blink::WebMediaDeviceInfoArray result;
for (const auto& device_info : device_infos) {
result.push_back(
TranslateMediaDeviceInfo(has_permission, salt_and_origin, device_info));
if (should_hide_device_ids_with_no_permission && !has_permission &&
result.back().device_id.empty()) {
break;
}
}
return result;
}
std::string CreateRandomMediaDeviceIDSalt() {
return base::UnguessableToken::Create().ToString();
}
void GetHMACFromRawDeviceId(GlobalRenderFrameHostId render_frame_host_id,
const std::string& raw_device_id,
DeviceIdCallback hmac_device_id_callback) {
auto got_salt = base::BindOnce(
[](const std::string& raw_device_id,
DeviceIdCallback hmac_device_id_callback,
const MediaDeviceSaltAndOrigin& salt_and_origin) {
std::move(hmac_device_id_callback)
.Run(GetHMACForRawMediaDeviceID(salt_and_origin, raw_device_id));
},
raw_device_id, std::move(hmac_device_id_callback));
GetMediaDeviceSaltAndOrigin(render_frame_host_id, std::move(got_salt));
}
void GetRawDeviceIdFromHMAC(GlobalRenderFrameHostId render_frame_host_id,
const std::string& hmac_device_id,
MediaDeviceType media_device_type,
OptionalDeviceIdCallback raw_device_id_callback) {
auto got_salt = base::BindOnce(
[](const std::string& hmac_device_id,
OptionalDeviceIdCallback raw_device_id_callback,
MediaDeviceType media_device_type,
const MediaDeviceSaltAndOrigin& salt_and_origin) {
// `GetRawDeviceIDForMediaDeviceHMAC()` needs to be called on the IO
// thread since it performs a device enumeration.
content::GetIOThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&GetRawDeviceIDForMediaDeviceHMAC, media_device_type,
salt_and_origin, hmac_device_id,
base::SequencedTaskRunner::GetCurrentDefault(),
std::move(raw_device_id_callback)));
},
hmac_device_id, std::move(raw_device_id_callback), media_device_type);
GetMediaDeviceSaltAndOrigin(render_frame_host_id, std::move(got_salt));
}
std::string GetHMACForRawMediaDeviceID(
const MediaDeviceSaltAndOrigin& salt_and_origin,
const std::string& raw_device_id,
bool use_group_salt) {
// TODO(crbug.com/1215532): DCHECKs are disabled during automated testing on
// CrOS and this check failed when tested on an experimental builder. Revert
// https://crrev.com/c/2932244 to enable it. See go/chrome-dcheck-on-cros
// or http://crbug.com/1113456 for more details.
#if !BUILDFLAG(IS_CHROMEOS_ASH)
DCHECK(!raw_device_id.empty());
#endif
if (raw_device_id == media::AudioDeviceDescription::kDefaultDeviceId ||
raw_device_id == media::AudioDeviceDescription::kCommunicationsDeviceId) {
return raw_device_id;
}
crypto::HMAC hmac(crypto::HMAC::SHA256);
const size_t digest_length = hmac.DigestLength();
std::vector<uint8_t> digest(digest_length);
bool result =
hmac.Init(salt_and_origin.origin().Serialize()) &&
hmac.Sign(
raw_device_id + (use_group_salt ? salt_and_origin.group_id_salt()
: salt_and_origin.device_id_salt()),
&digest[0], digest.size());
DCHECK(result);
return base::ToLowerASCII(base::HexEncode(&digest[0], digest.size()));
}
bool DoesRawMediaDeviceIDMatchHMAC(
const MediaDeviceSaltAndOrigin& salt_and_origin,
const std::string& hmac_device_id,
const std::string& raw_device_id) {
DCHECK(!raw_device_id.empty());
return GetHMACForRawMediaDeviceID(salt_and_origin, raw_device_id) ==
hmac_device_id;
}
void GetRawDeviceIDForMediaStreamHMAC(
MediaStreamType stream_type,
MediaDeviceSaltAndOrigin salt_and_origin,
std::string hmac_device_id,
scoped_refptr<base::SequencedTaskRunner> task_runner,
OptionalDeviceIdCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(stream_type == MediaStreamType::DEVICE_AUDIO_CAPTURE ||
stream_type == MediaStreamType::DEVICE_VIDEO_CAPTURE);
MediaDeviceType device_type = ConvertToMediaDeviceType(stream_type);
GetRawDeviceIDForMediaDeviceHMAC(device_type, std::move(salt_and_origin),
std::move(hmac_device_id),
std::move(task_runner), std::move(callback));
}
void GetRawDeviceIDForMediaDeviceHMAC(
MediaDeviceType device_type,
MediaDeviceSaltAndOrigin salt_and_origin,
std::string hmac_device_id,
scoped_refptr<base::SequencedTaskRunner> task_runner,
OptionalDeviceIdCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
MediaDevicesManager::BoolDeviceTypes requested_types;
requested_types[static_cast<size_t>(device_type)] = true;
MediaStreamManager::GetInstance()->media_devices_manager()->EnumerateDevices(
requested_types,
base::BindOnce(&FinalizeGetRawMediaDeviceIDForHMAC, device_type,
std::move(salt_and_origin), std::move(hmac_device_id),
std::move(task_runner), std::move(callback)));
}
} // namespace content