blob: f5eace41d4ff04f731bfa522465687c78a82ee4e [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/controlled_frame/controlled_frame_media_access_handler.h"
#include "base/types/expected.h"
#include "chrome/browser/media/webrtc/media_stream_device_permissions.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/pref_names.h"
#include "content/public/browser/media_stream_request.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 "extensions/browser/browser_frame_context_data.h"
#include "extensions/browser/guest_view/web_view/web_view_guest.h"
#include "services/network/public/cpp/permissions_policy/permissions_policy.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-shared.h"
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom-shared.h"
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
namespace controlled_frame {
namespace {
network::mojom::PermissionsPolicyFeature
GetPermissionPolicyFeatureForMediaStreamType(
blink::mojom::MediaStreamType type) {
switch (type) {
case blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE:
return network::mojom::PermissionsPolicyFeature::kMicrophone;
case blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE:
return network::mojom::PermissionsPolicyFeature::kCamera;
default:
return network::mojom::PermissionsPolicyFeature::kNotFound;
}
}
bool IsMediaStreamTypeSupported(blink::mojom::MediaStreamType type) {
return type == blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE ||
type == blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE;
}
} // namespace
ControlledFrameMediaAccessHandler::PendingMediaAccessRequestDetails::
PendingMediaAccessRequestDetails(const url::Origin& embedded_frame_origin,
blink::mojom::MediaStreamType type)
: embedded_frame_origin(embedded_frame_origin), type(type) {}
ControlledFrameMediaAccessHandler::ControlledFrameMediaAccessHandler() =
default;
ControlledFrameMediaAccessHandler::~ControlledFrameMediaAccessHandler() =
default;
bool ControlledFrameMediaAccessHandler::SupportsStreamType(
content::RenderFrameHost* render_frame_host,
const blink::mojom::MediaStreamType type,
const extensions::Extension* extension) {
if (!render_frame_host || extension) {
return false;
}
extensions::WebViewGuest* web_view =
extensions::WebViewGuest::FromRenderFrameHost(render_frame_host);
bool is_controlled_frame = web_view && web_view->attached() &&
web_view->IsOwnedByControlledFrameEmbedder();
return is_controlled_frame && IsMediaStreamTypeSupported(type);
}
bool ControlledFrameMediaAccessHandler::CheckMediaAccessPermission(
content::RenderFrameHost* render_frame_host,
const url::Origin& security_origin,
blink::mojom::MediaStreamType type,
const extensions::Extension* extension) {
CHECK(!extension);
extensions::WebViewGuest* web_view =
extensions::WebViewGuest::FromRenderFrameHost(render_frame_host);
CHECK(web_view);
if (!IsAllowedByPermissionsPolicy(web_view, security_origin, type)) {
return false;
}
const url::Origin& embedder_origin =
web_view->embedder_rfh()->GetLastCommittedOrigin();
const url::Origin& requesting_origin =
render_frame_host->GetLastCommittedOrigin();
// Technically, Controlled Frame permission check needs to be done
// asynchronously (via an event handled by the embedder). However, this method
// must return immediately. |requests_| is used as a caching mechanism. An
// embedder origin + requesting origin pair in |requests_| must have already
// passed the asynchronous checks at least once. Unfortunately, this means
// once a permission is granted, it cannot be revoked in the same session.
// Note that the type check can be omitted here because WebView Permission
// Request API does not differentiate audio and video requests, they are both
// treated as "media".
if (!requests_[embedder_origin].contains(requesting_origin)) {
return false;
}
return web_view->embedder_web_contents()->GetDelegate() &&
web_view->embedder_web_contents()
->GetDelegate()
->CheckMediaAccessPermission(web_view->embedder_rfh(),
embedder_origin, type);
}
void ControlledFrameMediaAccessHandler::HandleRequest(
content::WebContents* web_contents,
const content::MediaStreamRequest& request,
content::MediaResponseCallback callback,
const extensions::Extension* extension) {
CHECK(!extension);
content::RenderFrameHost* requesting_rfh = content::RenderFrameHost::FromID(
request.render_process_id, request.render_frame_id);
CHECK(requesting_rfh);
extensions::WebViewGuest* web_view =
extensions::WebViewGuest::FromRenderFrameHost(requesting_rfh);
CHECK(web_view);
CHECK(web_view->attached());
CHECK(web_view->IsOwnedByControlledFrameEmbedder());
const url::Origin& embedder_origin =
web_view->embedder_rfh()->GetLastCommittedOrigin();
const url::Origin& requesting_origin = request.url_origin;
requests_[embedder_origin].insert(requesting_origin);
if (request.audio_type !=
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE &&
request.video_type !=
blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE) {
std::move(callback).Run(
blink::mojom::StreamDevicesSet(),
blink::mojom::MediaStreamRequestResult::PERMISSION_DISMISSED,
std::unique_ptr<content::MediaStreamUI>());
return;
}
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
bool audio_denied =
request.audio_type ==
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE &&
(!IsAllowedByPermissionsPolicy(web_view, requesting_origin,
request.audio_type) ||
GetDevicePolicy(profile,
web_view->GetGuestMainFrame()->GetLastCommittedURL(),
prefs::kAudioCaptureAllowed,
prefs::kAudioCaptureAllowedUrls) == ALWAYS_DENY);
bool video_denied =
request.video_type ==
blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE &&
(!IsAllowedByPermissionsPolicy(web_view, requesting_origin,
request.video_type) ||
GetDevicePolicy(profile,
web_view->GetGuestMainFrame()->GetLastCommittedURL(),
prefs::kVideoCaptureAllowed,
prefs::kVideoCaptureAllowedUrls) == ALWAYS_DENY);
if (audio_denied || video_denied) {
std::move(callback).Run(
blink::mojom::StreamDevicesSet(),
blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED,
std::unique_ptr<content::MediaStreamUI>());
return;
}
content::GlobalRenderFrameHostId embedder_rfh_id =
web_view->embedder_rfh()->GetGlobalId();
content::MediaStreamRequest embedder_request = request;
embedder_request.render_process_id = embedder_rfh_id.child_id;
embedder_request.render_frame_id = embedder_rfh_id.frame_routing_id;
embedder_request.url_origin = embedder_origin;
embedder_request.security_origin = embedder_request.url_origin.GetURL();
if (!web_view->embedder_web_contents()->GetDelegate()) {
std::move(callback).Run(
blink::mojom::StreamDevicesSet(),
blink::mojom::MediaStreamRequestResult::FAILED_DUE_TO_SHUTDOWN,
std::unique_ptr<content::MediaStreamUI>());
return;
}
web_view->embedder_web_contents()
->GetDelegate()
->RequestMediaAccessPermission(web_view->embedder_web_contents(),
embedder_request, std::move(callback));
}
bool ControlledFrameMediaAccessHandler::IsAllowedByPermissionsPolicy(
extensions::WebViewGuest* web_view,
const url::Origin& requesting_origin,
blink::mojom::MediaStreamType type) {
if (!IsMediaStreamTypeSupported(type)) {
return false;
}
// Checks that embedder's permissions policy allows for both the embedder
// origin and requesting origin.
content::RenderFrameHost* embedder_rfh = web_view->embedder_rfh();
CHECK(embedder_rfh);
const network::PermissionsPolicy* permissions_policy =
embedder_rfh->GetPermissionsPolicy();
CHECK(permissions_policy);
if (!permissions_policy->IsFeatureEnabledForOrigin(
GetPermissionPolicyFeatureForMediaStreamType(type),
requesting_origin)) {
return false;
}
if (!permissions_policy->IsFeatureEnabledForOrigin(
GetPermissionPolicyFeatureForMediaStreamType(type),
embedder_rfh->GetLastCommittedOrigin())) {
return false;
}
return true;
}
} // namespace controlled_frame