910e62b5创建于 1月15日历史提交
// 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