| // Copyright 2016 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/media/capture_access_handler_base.h" |
| |
| #include <algorithm> |
| #include <string> |
| #include <utility> |
| |
| #include "build/build_config.h" |
| #include "chrome/browser/ui/ui_features.h" |
| #include "chrome/common/webui_url_constants.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "extensions/buildflags/buildflags.h" |
| #include "ui/gfx/native_widget_types.h" |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| #include "extensions/common/extension.h" |
| #endif |
| |
| #if defined(USE_AURA) |
| #include "ui/aura/window.h" |
| #endif |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "base/hash/sha1.h" |
| #include "base/strings/string_number_conversions.h" |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| using content::BrowserThread; |
| |
| // Tracks MEDIA_DESKTOP/TAB_VIDEO_CAPTURE sessions. Sessions are removed when |
| // MEDIA_REQUEST_STATE_CLOSING is encountered. |
| struct CaptureAccessHandlerBase::Session { |
| Session(int request_process_id, |
| int request_frame_id, |
| int page_request_id, |
| bool is_trusted, |
| bool is_capturing_link_secure, |
| content::DesktopMediaID::Type capturing_type, |
| gfx::NativeWindow target_window) |
| : request_process_id(request_process_id), |
| request_frame_id(request_frame_id), |
| page_request_id(page_request_id), |
| is_trusted(is_trusted), |
| is_capturing_link_secure(is_capturing_link_secure), |
| capturing_type(capturing_type), |
| target_window(target_window) { |
| SetWebContents(request_process_id, request_frame_id); |
| } |
| |
| void SetWebContents(int render_process_id, int render_frame_id) { |
| auto* web_contents = content::WebContents::FromRenderFrameHost( |
| content::RenderFrameHost::FromID(render_process_id, render_frame_id)); |
| // Use the outermost WebContents in the WebContents tree, if possible. |
| // If we can't find the WebContents, clear |target_web_contents|. |
| target_web_contents = |
| web_contents ? web_contents->GetOutermostWebContents()->GetWeakPtr() |
| : nullptr; |
| } |
| |
| content::WebContents* GetWebContents() const { |
| return target_web_contents.get(); |
| } |
| |
| int request_process_id; |
| int request_frame_id; |
| int page_request_id; |
| // Extensions control the routing of the captured MediaStream content. |
| // Therefore, only built-in extensions (and certain whitelisted ones) can be |
| // trusted to set-up secure links. |
| bool is_trusted; |
| |
| // This is true only if all connected video sinks are reported secure. |
| bool is_capturing_link_secure; |
| |
| // Reflects what this session is capturing. If the whole display is captured, |
| // then |capturing_type| is TYPE_SCREEN. If a specific window is specified, |
| // then |capturing_type| is TYPE_WINDOW and |target_window| is set. If a |
| // specific tab is the target, then |capturing_type| is TYPE_WEB_CONTENTS and |
| // |target_web_contents| is set. |
| content::DesktopMediaID::Type capturing_type; |
| gfx::NativeWindow target_window; |
| base::WeakPtr<content::WebContents> target_web_contents; |
| }; |
| |
| CaptureAccessHandlerBase::PendingAccessRequest::PendingAccessRequest( |
| std::unique_ptr<DesktopMediaPicker> picker, |
| const content::MediaStreamRequest& request, |
| content::MediaResponseCallback callback, |
| std::u16string application_title, |
| bool should_display_notification, |
| bool is_allowlisted_extension) |
| : picker(std::move(picker)), |
| request(request), |
| callback(std::move(callback)), |
| application_title(std::move(application_title)), |
| should_display_notification(should_display_notification), |
| is_allowlisted_extension(is_allowlisted_extension) {} |
| |
| CaptureAccessHandlerBase::PendingAccessRequest::~PendingAccessRequest() = |
| default; |
| |
| CaptureAccessHandlerBase::CaptureAccessHandlerBase() = default; |
| |
| CaptureAccessHandlerBase::~CaptureAccessHandlerBase() = default; |
| |
| void CaptureAccessHandlerBase::AddCaptureSession(int render_process_id, |
| int render_frame_id, |
| int page_request_id, |
| bool is_trusted) { |
| Session session = { |
| render_process_id, render_frame_id, page_request_id, is_trusted, true, |
| // Assume that the target is the same tab that is |
| // requesting capture, not the display or any particular |
| // window. This can be changed by calling UpdateTarget(). |
| content::DesktopMediaID::TYPE_WEB_CONTENTS, gfx::NativeWindow()}; |
| |
| sessions_.push_back(std::move(session)); |
| } |
| |
| void CaptureAccessHandlerBase::RemoveCaptureSession(int render_process_id, |
| int render_frame_id, |
| int page_request_id) { |
| auto it = FindSession(render_process_id, render_frame_id, page_request_id); |
| if (it != sessions_.end()) |
| sessions_.erase(it); |
| } |
| |
| std::list<CaptureAccessHandlerBase::Session>::iterator |
| CaptureAccessHandlerBase::FindSession(int render_process_id, |
| int render_frame_id, |
| int page_request_id) { |
| return std::ranges::find_if( |
| sessions_, [render_process_id, render_frame_id, |
| page_request_id](const Session& session) { |
| return session.request_process_id == render_process_id && |
| session.request_frame_id == render_frame_id && |
| session.page_request_id == page_request_id; |
| }); |
| } |
| |
| void CaptureAccessHandlerBase::UpdateMediaRequestState( |
| int render_process_id, |
| int render_frame_id, |
| int page_request_id, |
| blink::mojom::MediaStreamType stream_type, |
| content::MediaRequestState state) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| switch (stream_type) { |
| case blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE: |
| case blink::mojom::MediaStreamType::GUM_TAB_VIDEO_CAPTURE: |
| case blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE: |
| case blink::mojom::MediaStreamType::DISPLAY_AUDIO_CAPTURE: |
| case blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE_THIS_TAB: |
| case blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE_SET: |
| break; |
| default: |
| return; |
| } |
| |
| if (state == content::MEDIA_REQUEST_STATE_DONE) { |
| if (FindSession(render_process_id, render_frame_id, page_request_id) == |
| sessions_.end()) { |
| AddCaptureSession(render_process_id, render_frame_id, page_request_id, |
| false); |
| DVLOG(2) << "Add new session while UpdateMediaRequestState" |
| << " render_process_id: " << render_process_id |
| << " render_frame_id: " << render_frame_id |
| << " page_request_id: " << page_request_id; |
| } |
| } else if (state == content::MEDIA_REQUEST_STATE_CLOSING) { |
| RemoveCaptureSession(render_process_id, render_frame_id, page_request_id); |
| DVLOG(2) << "Remove session: " |
| << " render_process_id: " << render_process_id |
| << " render_frame_id: " << render_frame_id |
| << " page_request_id: " << page_request_id; |
| } |
| } |
| |
| void CaptureAccessHandlerBase::UpdateExtensionTrusted( |
| const content::MediaStreamRequest& request, |
| bool is_allowlisted_extension) { |
| UpdateTrusted(request, is_allowlisted_extension || |
| IsBuiltInFeedbackUI(request.security_origin)); |
| } |
| |
| void CaptureAccessHandlerBase::UpdateTrusted( |
| const content::MediaStreamRequest& request, |
| bool is_trusted) { |
| auto it = FindSession(request.render_process_id, request.render_frame_id, |
| request.page_request_id); |
| if (it != sessions_.end()) { |
| it->is_trusted = is_trusted; |
| DVLOG(2) << "CaptureAccessHandlerBase::UpdateTrusted" |
| << " render_process_id: " << request.render_process_id |
| << " render_frame_id: " << request.render_frame_id |
| << " page_request_id: " << request.page_request_id |
| << " is_trusted: " << is_trusted; |
| return; |
| } |
| |
| AddCaptureSession(request.render_process_id, request.render_frame_id, |
| request.page_request_id, is_trusted); |
| DVLOG(2) << "Add new session while UpdateTrusted" |
| << " render_process_id: " << request.render_process_id |
| << " render_frame_id: " << request.render_frame_id |
| << " page_request_id: " << request.page_request_id |
| << " is_trusted: " << is_trusted; |
| } |
| |
| void CaptureAccessHandlerBase::UpdateTarget( |
| const content::MediaStreamRequest& request, |
| const content::DesktopMediaID& target) { |
| DVLOG(2) << __func__ << " requestor: " << request.render_process_id << ":" |
| << request.render_frame_id << ", target: " << target.ToString(); |
| |
| auto it = FindSession(request.render_process_id, request.render_frame_id, |
| request.page_request_id); |
| if (it == sessions_.end()) { |
| DLOG(WARNING) << "UpdateTarget() can not find the session."; |
| return; |
| } |
| |
| // Update target fields. |
| it->capturing_type = target.type; |
| if (target.type == content::DesktopMediaID::TYPE_WINDOW) { |
| // If this is the Chrome window, then any tab in this window could be |
| // captured. |
| // TODO(crbug.com/41396679): Implement this for MacOS. |
| #if defined(USE_AURA) |
| it->target_window = content::DesktopMediaID::GetNativeWindowById(target); |
| #endif |
| } else if (target.type == content::DesktopMediaID::TYPE_WEB_CONTENTS) { |
| // Specific tab captured. |
| it->SetWebContents(target.web_contents_id.render_process_id, |
| target.web_contents_id.main_render_frame_id); |
| } |
| } |
| |
| bool CaptureAccessHandlerBase::IsInsecureCapturingInProgress( |
| int render_process_id, |
| int render_frame_id) { |
| DVLOG(3) << __func__ << " checking for " << render_process_id << ":" |
| << render_frame_id; |
| if (sessions_.empty()) |
| return false; |
| |
| // Check if the frame is being captured insecurely as the target of any |
| // session. |
| for (const Session& session : sessions_) { |
| if (MatchesSession(session, render_process_id, render_frame_id)) |
| if (!session.is_trusted || !session.is_capturing_link_secure) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| // static |
| bool CaptureAccessHandlerBase::MatchesSession(const Session& session, |
| int target_process_id, |
| int target_frame_id) { |
| switch (session.capturing_type) { |
| case content::DesktopMediaID::TYPE_NONE: |
| // Session has no target, so it doesn't match. |
| return false; |
| |
| case content::DesktopMediaID::TYPE_SCREEN: |
| // Capturing the whole screen. |
| return true; |
| |
| case content::DesktopMediaID::TYPE_WINDOW: |
| #if defined(USE_AURA) |
| { |
| // Check if the window hosting this frame is the target. |
| auto* host = |
| content::RenderFrameHost::FromID(target_process_id, target_frame_id); |
| if (!host) |
| return false; |
| |
| gfx::NativeWindow window = host->GetNativeView()->GetRootWindow(); |
| return window && window == session.target_window; |
| } |
| #else |
| // Unable to determine the window. |
| // TODO(crbug.com/41396679): Implement this for MacOS. |
| return false; |
| #endif // defined(USE_AURA) |
| |
| case content::DesktopMediaID::TYPE_WEB_CONTENTS: |
| // Check that the target is the frame selected. |
| auto* target_web_contents = session.GetWebContents(); |
| if (!target_web_contents) |
| return false; |
| auto* web_contents = content::WebContents::FromRenderFrameHost( |
| content::RenderFrameHost::FromID(target_process_id, target_frame_id)); |
| if (!web_contents) |
| return false; |
| return target_web_contents == web_contents->GetOutermostWebContents(); |
| } |
| |
| NOTREACHED(); |
| } |
| |
| void CaptureAccessHandlerBase::UpdateVideoScreenCaptureStatus( |
| int render_process_id, |
| int render_frame_id, |
| int page_request_id, |
| bool is_secure) { |
| auto it = FindSession(render_process_id, render_frame_id, page_request_id); |
| if (it != sessions_.end()) { |
| it->is_capturing_link_secure = is_secure; |
| DVLOG(2) << "UpdateVideoScreenCaptureStatus:" |
| << " render_process_id: " << render_process_id |
| << " render_frame_id: " << render_frame_id |
| << " page_request_id: " << page_request_id |
| << " is_capturing_link_secure: " << is_secure; |
| } |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| bool CaptureAccessHandlerBase::IsExtensionAllowedForScreenCapture( |
| const extensions::Extension* extension) { |
| if (!extension) |
| return false; |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| std::string hash = base::SHA1HashString(extension->id()); |
| std::string hex_hash = base::HexEncode(hash); |
| |
| // crbug.com/446688 |
| return hex_hash == "4F25792AF1AA7483936DE29C07806F203C7170A0" || |
| hex_hash == "BD8781D757D830FC2E85470A1B6E8A718B7EE0D9" || |
| hex_hash == "4AC2B6C63C6480D150DFDA13E4A5956EB1D0DDBB" || |
| hex_hash == "81986D4F846CEDDDB962643FA501D1780DD441BB"; |
| #else |
| return false; |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| bool CaptureAccessHandlerBase::IsBuiltInFeedbackUI(const GURL& origin) { |
| return origin.spec() == chrome::kChromeUIFeedbackURL; |
| } |