| // Copyright 2013 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. |
| |
| #include "content/browser/renderer_host/media/media_stream_ui_proxy.h" |
| |
| #include <string> |
| #include <utility> |
| |
| #include "base/message_loop/message_loop.h" |
| #include "base/run_loop.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "build/build_config.h" |
| #include "content/browser/frame_host/render_frame_host_delegate.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/test/test_browser_thread.h" |
| #include "content/test/test_render_frame_host.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| using testing::_; |
| using testing::Return; |
| using testing::SaveArg; |
| |
| namespace content { |
| |
| namespace { |
| class MockRenderFrameHostDelegate : public RenderFrameHostDelegate { |
| public: |
| void RequestMediaAccessPermission(const MediaStreamRequest& request, |
| MediaResponseCallback callback) { |
| return RequestMediaAccessPermission(request, &callback); |
| } |
| MOCK_METHOD2(RequestMediaAccessPermission, |
| void(const MediaStreamRequest& request, |
| MediaResponseCallback* callback)); |
| MOCK_METHOD3(CheckMediaAccessPermission, |
| bool(RenderFrameHost* render_frame_host, |
| const url::Origin& security_origin, |
| MediaStreamType type)); |
| }; |
| |
| class MockResponseCallback { |
| public: |
| MOCK_METHOD2(OnAccessRequestResponse, |
| void(const MediaStreamDevices& devices, |
| content::MediaStreamRequestResult result)); |
| MOCK_METHOD1(OnCheckResponse, void(bool have_access)); |
| }; |
| |
| class MockMediaStreamUI : public MediaStreamUI { |
| public: |
| MOCK_METHOD1(OnStarted, gfx::NativeViewId(const base::Closure& stop)); |
| }; |
| |
| class MockStopStreamHandler { |
| public: |
| MOCK_METHOD0(OnStop, void()); |
| MOCK_METHOD1(OnWindowId, void(gfx::NativeViewId window_id)); |
| }; |
| |
| |
| } // namespace |
| |
| class MediaStreamUIProxyTest : public testing::Test { |
| public: |
| MediaStreamUIProxyTest() |
| : ui_thread_(BrowserThread::UI, &message_loop_), |
| io_thread_(BrowserThread::IO, &message_loop_) { |
| proxy_ = MediaStreamUIProxy::CreateForTests(&delegate_); |
| } |
| |
| ~MediaStreamUIProxyTest() override { |
| proxy_.reset(); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| protected: |
| base::MessageLoop message_loop_; |
| TestBrowserThread ui_thread_; |
| TestBrowserThread io_thread_; |
| |
| MockRenderFrameHostDelegate delegate_; |
| MockResponseCallback response_callback_; |
| std::unique_ptr<MediaStreamUIProxy> proxy_; |
| }; |
| |
| MATCHER_P(SameRequest, expected, "") { |
| return |
| expected->render_process_id == arg.render_process_id && |
| expected->render_frame_id == arg.render_frame_id && |
| expected->security_origin == arg.security_origin && |
| expected->request_type == arg.request_type && |
| expected->requested_audio_device_id == arg.requested_audio_device_id && |
| expected->requested_video_device_id == arg.requested_video_device_id && |
| expected->audio_type == arg.audio_type && |
| expected->video_type == arg.video_type; |
| } |
| |
| // These tests are flaky on Linux. https://crbug.com/826483 |
| #if defined(OS_LINUX) |
| #define MAYBE_DeleteBeforeAccepted DISABLED_DeleteBeforeAccepted |
| #define MAYBE_Deny DISABLED_Deny |
| #define MAYBE_AcceptAndStart DISABLED_AcceptAndStart |
| #define MAYBE_StopFromUI DISABLED_StopFromUI |
| #define MAYBE_WindowIdCallbackCalled DISABLED_WindowIdCallbackCalled |
| #else |
| #define MAYBE_DeleteBeforeAccepted DeleteBeforeAccepted |
| #define MAYBE_Deny Deny |
| #define MAYBE_AcceptAndStart AcceptAndStart |
| #define MAYBE_StopFromUI StopFromUI |
| #define MAYBE_WindowIdCallbackCalled WindowIdCallbackCalled |
| #endif |
| |
| TEST_F(MediaStreamUIProxyTest, MAYBE_Deny) { |
| std::unique_ptr<MediaStreamRequest> request(new MediaStreamRequest( |
| 0, 0, 0, GURL("http://origin/"), false, MEDIA_GENERATE_STREAM, |
| std::string(), std::string(), MEDIA_DEVICE_AUDIO_CAPTURE, |
| MEDIA_DEVICE_VIDEO_CAPTURE, false)); |
| MediaStreamRequest* request_ptr = request.get(); |
| proxy_->RequestAccess( |
| std::move(request), |
| base::BindOnce(&MockResponseCallback::OnAccessRequestResponse, |
| base::Unretained(&response_callback_))); |
| MediaResponseCallback callback; |
| EXPECT_CALL(delegate_, |
| RequestMediaAccessPermission(SameRequest(request_ptr), _)) |
| .WillOnce([&](testing::Unused, MediaResponseCallback* cb) { |
| callback = std::move(*cb); |
| }); |
| base::RunLoop().RunUntilIdle(); |
| ASSERT_FALSE(callback.is_null()); |
| |
| MediaStreamDevices devices; |
| std::move(callback).Run(devices, MEDIA_DEVICE_OK, |
| std::unique_ptr<MediaStreamUI>()); |
| |
| MediaStreamDevices response; |
| EXPECT_CALL(response_callback_, OnAccessRequestResponse(_, _)) |
| .WillOnce(SaveArg<0>(&response)); |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_TRUE(response.empty()); |
| } |
| |
| TEST_F(MediaStreamUIProxyTest, MAYBE_AcceptAndStart) { |
| std::unique_ptr<MediaStreamRequest> request(new MediaStreamRequest( |
| 0, 0, 0, GURL("http://origin/"), false, MEDIA_GENERATE_STREAM, |
| std::string(), std::string(), MEDIA_DEVICE_AUDIO_CAPTURE, |
| MEDIA_DEVICE_VIDEO_CAPTURE, false)); |
| MediaStreamRequest* request_ptr = request.get(); |
| proxy_->RequestAccess( |
| std::move(request), |
| base::BindOnce(&MockResponseCallback::OnAccessRequestResponse, |
| base::Unretained(&response_callback_))); |
| MediaResponseCallback callback; |
| EXPECT_CALL(delegate_, |
| RequestMediaAccessPermission(SameRequest(request_ptr), _)) |
| .WillOnce([&](testing::Unused, MediaResponseCallback* cb) { |
| callback = std::move(*cb); |
| }); |
| base::RunLoop().RunUntilIdle(); |
| ASSERT_FALSE(callback.is_null()); |
| |
| MediaStreamDevices devices; |
| devices.push_back( |
| MediaStreamDevice(MEDIA_DEVICE_AUDIO_CAPTURE, "Mic", "Mic")); |
| std::unique_ptr<MockMediaStreamUI> ui(new MockMediaStreamUI()); |
| EXPECT_CALL(*ui, OnStarted(_)).WillOnce(Return(0)); |
| std::move(callback).Run(devices, MEDIA_DEVICE_OK, std::move(ui)); |
| |
| MediaStreamDevices response; |
| EXPECT_CALL(response_callback_, OnAccessRequestResponse(_, _)) |
| .WillOnce(SaveArg<0>(&response)); |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_FALSE(response.empty()); |
| |
| proxy_->OnStarted(base::Closure(), MediaStreamUIProxy::WindowIdCallback()); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| // Verify that the proxy can be deleted before the request is processed. |
| TEST_F(MediaStreamUIProxyTest, MAYBE_DeleteBeforeAccepted) { |
| std::unique_ptr<MediaStreamRequest> request(new MediaStreamRequest( |
| 0, 0, 0, GURL("http://origin/"), false, MEDIA_GENERATE_STREAM, |
| std::string(), std::string(), MEDIA_DEVICE_AUDIO_CAPTURE, |
| MEDIA_DEVICE_VIDEO_CAPTURE, false)); |
| MediaStreamRequest* request_ptr = request.get(); |
| proxy_->RequestAccess( |
| std::move(request), |
| base::BindOnce(&MockResponseCallback::OnAccessRequestResponse, |
| base::Unretained(&response_callback_))); |
| MediaResponseCallback callback; |
| EXPECT_CALL(delegate_, |
| RequestMediaAccessPermission(SameRequest(request_ptr), _)) |
| .WillOnce([&](testing::Unused, MediaResponseCallback* cb) { |
| callback = std::move(*cb); |
| }); |
| base::RunLoop().RunUntilIdle(); |
| ASSERT_FALSE(callback.is_null()); |
| |
| proxy_.reset(); |
| |
| MediaStreamDevices devices; |
| std::unique_ptr<MediaStreamUI> ui; |
| std::move(callback).Run(devices, MEDIA_DEVICE_OK, std::move(ui)); |
| } |
| |
| TEST_F(MediaStreamUIProxyTest, MAYBE_StopFromUI) { |
| std::unique_ptr<MediaStreamRequest> request(new MediaStreamRequest( |
| 0, 0, 0, GURL("http://origin/"), false, MEDIA_GENERATE_STREAM, |
| std::string(), std::string(), MEDIA_DEVICE_AUDIO_CAPTURE, |
| MEDIA_DEVICE_VIDEO_CAPTURE, false)); |
| MediaStreamRequest* request_ptr = request.get(); |
| proxy_->RequestAccess( |
| std::move(request), |
| base::BindOnce(&MockResponseCallback::OnAccessRequestResponse, |
| base::Unretained(&response_callback_))); |
| MediaResponseCallback callback; |
| EXPECT_CALL(delegate_, |
| RequestMediaAccessPermission(SameRequest(request_ptr), _)) |
| .WillOnce([&](testing::Unused, MediaResponseCallback* cb) { |
| callback = std::move(*cb); |
| }); |
| base::RunLoop().RunUntilIdle(); |
| ASSERT_FALSE(callback.is_null()); |
| |
| base::Closure stop_callback; |
| |
| MediaStreamDevices devices; |
| devices.push_back( |
| MediaStreamDevice(MEDIA_DEVICE_AUDIO_CAPTURE, "Mic", "Mic")); |
| std::unique_ptr<MockMediaStreamUI> ui(new MockMediaStreamUI()); |
| EXPECT_CALL(*ui, OnStarted(_)) |
| .WillOnce(testing::DoAll(SaveArg<0>(&stop_callback), Return(0))); |
| std::move(callback).Run(devices, MEDIA_DEVICE_OK, std::move(ui)); |
| |
| MediaStreamDevices response; |
| EXPECT_CALL(response_callback_, OnAccessRequestResponse(_, _)) |
| .WillOnce(SaveArg<0>(&response)); |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_FALSE(response.empty()); |
| |
| MockStopStreamHandler stop_handler; |
| proxy_->OnStarted(base::BindOnce(&MockStopStreamHandler::OnStop, |
| base::Unretained(&stop_handler)), |
| MediaStreamUIProxy::WindowIdCallback()); |
| base::RunLoop().RunUntilIdle(); |
| |
| ASSERT_FALSE(stop_callback.is_null()); |
| EXPECT_CALL(stop_handler, OnStop()); |
| stop_callback.Run(); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| TEST_F(MediaStreamUIProxyTest, MAYBE_WindowIdCallbackCalled) { |
| std::unique_ptr<MediaStreamRequest> request(new MediaStreamRequest( |
| 0, 0, 0, GURL("http://origin/"), false, MEDIA_GENERATE_STREAM, |
| std::string(), std::string(), MEDIA_NO_SERVICE, |
| MEDIA_GUM_DESKTOP_VIDEO_CAPTURE, false)); |
| MediaStreamRequest* request_ptr = request.get(); |
| |
| proxy_->RequestAccess( |
| std::move(request), |
| base::BindOnce(&MockResponseCallback::OnAccessRequestResponse, |
| base::Unretained(&response_callback_))); |
| MediaResponseCallback callback; |
| EXPECT_CALL(delegate_, |
| RequestMediaAccessPermission(SameRequest(request_ptr), _)) |
| .WillOnce([&](testing::Unused, MediaResponseCallback* cb) { |
| callback = std::move(*cb); |
| }); |
| base::RunLoop().RunUntilIdle(); |
| |
| const int kWindowId = 1; |
| std::unique_ptr<MockMediaStreamUI> ui(new MockMediaStreamUI()); |
| EXPECT_CALL(*ui, OnStarted(_)).WillOnce(Return(kWindowId)); |
| |
| std::move(callback).Run(MediaStreamDevices(), MEDIA_DEVICE_OK, std::move(ui)); |
| EXPECT_CALL(response_callback_, OnAccessRequestResponse(_, _)); |
| |
| MockStopStreamHandler handler; |
| EXPECT_CALL(handler, OnWindowId(kWindowId)); |
| |
| proxy_->OnStarted(base::BindOnce(&MockStopStreamHandler::OnStop, |
| base::Unretained(&handler)), |
| base::BindOnce(&MockStopStreamHandler::OnWindowId, |
| base::Unretained(&handler))); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| // Basic tests for feature policy checks through the MediaStreamUIProxy. These |
| // tests are not meant to cover every edge case as the FeaturePolicy class |
| // itself is tested thoroughly in feature_policy_unittest.cc and in |
| // render_frame_host_feature_policy_unittest.cc. |
| class MediaStreamUIProxyFeaturePolicyTest |
| : public RenderViewHostImplTestHarness { |
| public: |
| void SetUp() override { |
| RenderViewHostImplTestHarness::SetUp(); |
| NavigateAndCommit(GURL("https://example.com")); |
| } |
| |
| protected: |
| // The header policy should only be set once on page load, so we refresh the |
| // page to simulate that. |
| void RefreshPageAndSetHeaderPolicy(RenderFrameHost* rfh, |
| blink::mojom::FeaturePolicyFeature feature, |
| bool enabled) { |
| NavigateAndCommit(rfh->GetLastCommittedURL()); |
| std::vector<url::Origin> whitelist; |
| if (enabled) |
| whitelist.push_back(rfh->GetLastCommittedOrigin()); |
| RenderFrameHostTester::For(rfh)->SimulateFeaturePolicyHeader(feature, |
| whitelist); |
| } |
| |
| void GetResultForRequest(std::unique_ptr<MediaStreamRequest> request, |
| MediaStreamDevices* devices_out, |
| MediaStreamRequestResult* result_out) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| base::RunLoop run_loop; |
| quit_closure_ = run_loop.QuitClosure(); |
| BrowserThread::PostTask( |
| BrowserThread::IO, FROM_HERE, |
| base::BindOnce( |
| &MediaStreamUIProxyFeaturePolicyTest::GetResultForRequestOnIOThread, |
| base::Unretained(this), std::move(request))); |
| run_loop.Run(); |
| *devices_out = devices_; |
| *result_out = result_; |
| } |
| |
| std::unique_ptr<MediaStreamRequest> CreateRequest(RenderFrameHost* rfh, |
| MediaStreamType mic_type, |
| MediaStreamType cam_type) { |
| return std::make_unique<MediaStreamRequest>( |
| rfh->GetProcess()->GetID(), rfh->GetRoutingID(), 0, |
| rfh->GetLastCommittedURL(), false, MEDIA_GENERATE_STREAM, std::string(), |
| std::string(), mic_type, cam_type, false); |
| } |
| |
| private: |
| class TestRFHDelegate : public RenderFrameHostDelegate { |
| void RequestMediaAccessPermission(const MediaStreamRequest& request, |
| MediaResponseCallback callback) override { |
| MediaStreamDevices devices; |
| if (request.audio_type == MEDIA_DEVICE_AUDIO_CAPTURE) { |
| devices.push_back( |
| MediaStreamDevice(MEDIA_DEVICE_AUDIO_CAPTURE, "Mic", "Mic")); |
| } |
| if (request.video_type == MEDIA_DEVICE_VIDEO_CAPTURE) { |
| devices.push_back( |
| MediaStreamDevice(MEDIA_DEVICE_VIDEO_CAPTURE, "Camera", "Camera")); |
| } |
| std::unique_ptr<MockMediaStreamUI> ui(new MockMediaStreamUI()); |
| std::move(callback).Run(devices, MEDIA_DEVICE_OK, std::move(ui)); |
| } |
| }; |
| |
| void GetResultForRequestOnIOThread( |
| std::unique_ptr<MediaStreamRequest> request) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| proxy_ = MediaStreamUIProxy::CreateForTests(&delegate_); |
| proxy_->RequestAccess( |
| std::move(request), |
| base::BindOnce( |
| &MediaStreamUIProxyFeaturePolicyTest::FinishedGetResultOnIOThread, |
| base::Unretained(this))); |
| } |
| |
| void FinishedGetResultOnIOThread(const MediaStreamDevices& devices, |
| MediaStreamRequestResult result) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| proxy_.reset(); |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::BindOnce(&MediaStreamUIProxyFeaturePolicyTest::FinishedGetResult, |
| base::Unretained(this), devices, result)); |
| } |
| |
| void FinishedGetResult(const MediaStreamDevices& devices, |
| MediaStreamRequestResult result) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| devices_ = devices; |
| result_ = result; |
| quit_closure_.Run(); |
| } |
| |
| // These should only be accessed on the UI thread. |
| MediaStreamDevices devices_; |
| MediaStreamRequestResult result_; |
| base::Closure quit_closure_; |
| |
| // These should only be accessed on the IO thread. |
| TestRFHDelegate delegate_; |
| std::unique_ptr<MediaStreamUIProxy> proxy_; |
| }; |
| |
| TEST_F(MediaStreamUIProxyFeaturePolicyTest, FeaturePolicy) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature(features::kUseFeaturePolicyForPermissions); |
| MediaStreamDevices devices; |
| MediaStreamRequestResult result; |
| |
| // Default FP. |
| GetResultForRequest(CreateRequest(main_rfh(), MEDIA_DEVICE_AUDIO_CAPTURE, |
| MEDIA_DEVICE_VIDEO_CAPTURE), |
| &devices, &result); |
| EXPECT_EQ(MEDIA_DEVICE_OK, result); |
| ASSERT_EQ(2u, devices.size()); |
| EXPECT_EQ(MEDIA_DEVICE_AUDIO_CAPTURE, devices[0].type); |
| EXPECT_EQ(MEDIA_DEVICE_VIDEO_CAPTURE, devices[1].type); |
| |
| // Mic disabled. |
| RefreshPageAndSetHeaderPolicy(main_rfh(), |
| blink::mojom::FeaturePolicyFeature::kMicrophone, |
| /*enabled=*/false); |
| GetResultForRequest(CreateRequest(main_rfh(), MEDIA_DEVICE_AUDIO_CAPTURE, |
| MEDIA_DEVICE_VIDEO_CAPTURE), |
| &devices, &result); |
| EXPECT_EQ(MEDIA_DEVICE_OK, result); |
| ASSERT_EQ(1u, devices.size()); |
| EXPECT_EQ(MEDIA_DEVICE_VIDEO_CAPTURE, devices[0].type); |
| |
| // Camera disabled. |
| RefreshPageAndSetHeaderPolicy(main_rfh(), |
| blink::mojom::FeaturePolicyFeature::kCamera, |
| /*enabled=*/false); |
| GetResultForRequest(CreateRequest(main_rfh(), MEDIA_DEVICE_AUDIO_CAPTURE, |
| MEDIA_DEVICE_VIDEO_CAPTURE), |
| &devices, &result); |
| EXPECT_EQ(MEDIA_DEVICE_OK, result); |
| ASSERT_EQ(1u, devices.size()); |
| EXPECT_EQ(MEDIA_DEVICE_AUDIO_CAPTURE, devices[0].type); |
| |
| // Camera disabled resulting in no devices being returned. |
| RefreshPageAndSetHeaderPolicy(main_rfh(), |
| blink::mojom::FeaturePolicyFeature::kCamera, |
| /*enabled=*/false); |
| GetResultForRequest( |
| CreateRequest(main_rfh(), MEDIA_NO_SERVICE, MEDIA_DEVICE_VIDEO_CAPTURE), |
| &devices, &result); |
| EXPECT_EQ(MEDIA_DEVICE_PERMISSION_DENIED, result); |
| ASSERT_EQ(0u, devices.size()); |
| |
| // Ensure that the policy is ignored if kUseFeaturePolicyForPermissions is |
| // disabled. |
| base::test::ScopedFeatureList empty_feature_list; |
| empty_feature_list.InitAndDisableFeature( |
| features::kUseFeaturePolicyForPermissions); |
| GetResultForRequest(CreateRequest(main_rfh(), MEDIA_DEVICE_AUDIO_CAPTURE, |
| MEDIA_DEVICE_VIDEO_CAPTURE), |
| &devices, &result); |
| EXPECT_EQ(MEDIA_DEVICE_OK, result); |
| ASSERT_EQ(2u, devices.size()); |
| EXPECT_EQ(MEDIA_DEVICE_AUDIO_CAPTURE, devices[0].type); |
| EXPECT_EQ(MEDIA_DEVICE_VIDEO_CAPTURE, devices[1].type); |
| } |
| |
| } // namespace content |