// Copyright (c) 2012 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 "chrome/browser/media/media_stream_devices_controller.h" #include "base/metrics/histogram.h" #include "base/prefs/scoped_user_pref_update.h" #include "base/strings/utf_string_conversions.h" #include "base/values.h" #include "chrome/browser/content_settings/host_content_settings_map.h" #include "chrome/browser/content_settings/tab_specific_content_settings.h" #include "chrome/browser/media/media_capture_devices_dispatcher.h" #include "chrome/browser/media/media_stream_capture_indicator.h" #include "chrome/browser/media/media_stream_device_permissions.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/browser.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/pref_names.h" #include "chrome/grit/generated_resources.h" #include "components/content_settings/core/browser/content_settings_provider.h" #include "components/content_settings/core/common/content_settings.h" #include "components/content_settings/core/common/content_settings_pattern.h" #include "components/pref_registry/pref_registry_syncable.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/render_widget_host_view.h" #include "content/public/common/media_stream_request.h" #include "extensions/common/constants.h" #include "grit/theme_resources.h" #include "ui/base/l10n/l10n_util.h" using content::BrowserThread; namespace { bool HasAvailableDevicesForRequest(const content::MediaStreamRequest& request) { const content::MediaStreamDevices* audio_devices = request.audio_type == content::MEDIA_DEVICE_AUDIO_CAPTURE ? &MediaCaptureDevicesDispatcher::GetInstance() ->GetAudioCaptureDevices() : NULL; const content::MediaStreamDevices* video_devices = request.video_type == content::MEDIA_DEVICE_VIDEO_CAPTURE ? &MediaCaptureDevicesDispatcher::GetInstance() ->GetVideoCaptureDevices() : NULL; // Check if we're being asked for audio and/or video and that either of those // lists is empty. If they are, we do not have devices available for the // request. // TODO(tommi): It's kind of strange to have this here since if we fail this // test, there'll be a UI shown that indicates to the user that access to // non-existing audio/video devices has been denied. The user won't have // any way to change that but there will be a UI shown which indicates that // access is blocked. if ((audio_devices != NULL && audio_devices->empty()) || (video_devices != NULL && video_devices->empty())) { return false; } // Note: we check requested_[audio|video]_device_id before dereferencing // [audio|video]_devices. If the requested device id is non-empty, then // the corresponding device list must not be NULL. if (!request.requested_audio_device_id.empty() && !audio_devices->FindById(request.requested_audio_device_id)) { return false; } if (!request.requested_video_device_id.empty() && !video_devices->FindById(request.requested_video_device_id)) { return false; } return true; } enum DevicePermissionActions { kAllowHttps = 0, kAllowHttp, kDeny, kCancel, kPermissionActionsMax // Must always be last! }; } // namespace MediaStreamDevicesController::MediaStreamTypeSettings::MediaStreamTypeSettings( Permission permission, const std::string& requested_device_id): permission(permission), requested_device_id(requested_device_id) {} MediaStreamDevicesController::MediaStreamTypeSettings:: MediaStreamTypeSettings(): permission(MEDIA_NONE) {} MediaStreamDevicesController::MediaStreamTypeSettings:: ~MediaStreamTypeSettings() {} MediaStreamDevicesController::MediaStreamDevicesController( content::WebContents* web_contents, const content::MediaStreamRequest& request, const content::MediaResponseCallback& callback) : web_contents_(web_contents), request_(request), callback_(callback) { profile_ = Profile::FromBrowserContext(web_contents->GetBrowserContext()); content_settings_ = TabSpecificContentSettings::FromWebContents(web_contents); // For MEDIA_OPEN_DEVICE requests (Pepper) we always request both webcam // and microphone to avoid popping two infobars. // We start with setting the requested media type to allowed or blocked // depending on the policy. If not blocked by policy it may be blocked later // in the two remaining filtering steps (by user setting or by user when // clicking the infobar). // TODO(grunell): It's not the nicest solution to let the MEDIA_OPEN_DEVICE // case take a ride on the MEDIA_DEVICE_*_CAPTURE permission. Should be fixed. if (request.audio_type == content::MEDIA_DEVICE_AUDIO_CAPTURE || request.request_type == content::MEDIA_OPEN_DEVICE) { if (GetDevicePolicy(profile_, request_.security_origin, prefs::kAudioCaptureAllowed, prefs::kAudioCaptureAllowedUrls) == ALWAYS_DENY) { request_permissions_.insert(std::make_pair( content::MEDIA_DEVICE_AUDIO_CAPTURE, MediaStreamTypeSettings(MEDIA_BLOCKED_BY_POLICY, request.requested_audio_device_id))); } else { request_permissions_.insert(std::make_pair( content::MEDIA_DEVICE_AUDIO_CAPTURE, MediaStreamTypeSettings(MEDIA_ALLOWED, request.requested_audio_device_id))); } } if (request.video_type == content::MEDIA_DEVICE_VIDEO_CAPTURE || request.request_type == content::MEDIA_OPEN_DEVICE) { if (GetDevicePolicy(profile_, request_.security_origin, prefs::kVideoCaptureAllowed, prefs::kVideoCaptureAllowedUrls) == ALWAYS_DENY) { request_permissions_.insert(std::make_pair( content::MEDIA_DEVICE_VIDEO_CAPTURE, MediaStreamTypeSettings(MEDIA_BLOCKED_BY_POLICY, request.requested_video_device_id))); } else { request_permissions_.insert(std::make_pair( content::MEDIA_DEVICE_VIDEO_CAPTURE, MediaStreamTypeSettings(MEDIA_ALLOWED, request.requested_video_device_id))); } } } MediaStreamDevicesController::~MediaStreamDevicesController() { if (!callback_.is_null()) { callback_.Run(content::MediaStreamDevices(), content::MEDIA_DEVICE_INVALID_STATE, scoped_ptr()); } } // static void MediaStreamDevicesController::RegisterProfilePrefs( user_prefs::PrefRegistrySyncable* prefs) { prefs->RegisterBooleanPref(prefs::kVideoCaptureAllowed, true, user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF); prefs->RegisterBooleanPref(prefs::kAudioCaptureAllowed, true, user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF); prefs->RegisterListPref(prefs::kVideoCaptureAllowedUrls, user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF); prefs->RegisterListPref(prefs::kAudioCaptureAllowedUrls, user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF); } // TODO(gbillock): rename? doesn't actually dismiss. More of a 'check profile // and system for compatibility' thing. bool MediaStreamDevicesController::DismissInfoBarAndTakeActionOnSettings() { // Tab capture is allowed for extensions only and infobar is not shown for // extensions. if (request_.audio_type == content::MEDIA_TAB_AUDIO_CAPTURE || request_.video_type == content::MEDIA_TAB_VIDEO_CAPTURE) { Deny(false, content::MEDIA_DEVICE_INVALID_STATE); return true; } // Deny the request if the security origin is empty, this happens with // file access without |--allow-file-access-from-files| flag. if (request_.security_origin.is_empty()) { Deny(false, content::MEDIA_DEVICE_INVALID_SECURITY_ORIGIN); return true; } // Deny the request if there is no device attached to the OS of the // requested type. If both audio and video is requested, both types must be // available. if (!HasAvailableDevicesForRequest(request_)) { Deny(false, content::MEDIA_DEVICE_NO_HARDWARE); return true; } // Check if any allow exception has been made for this request. if (IsRequestAllowedByDefault()) { Accept(false); return true; } // Filter any parts of the request that have been blocked by default and deny // it if nothing is left to accept. if (FilterBlockedByDefaultDevices() == 0) { Deny(false, content::MEDIA_DEVICE_PERMISSION_DENIED); return true; } // Check if the media default setting is set to block. if (IsDefaultMediaAccessBlocked()) { Deny(false, content::MEDIA_DEVICE_PERMISSION_DENIED); return true; } // Show the infobar. return false; } bool MediaStreamDevicesController::HasAudio() const { return IsDeviceAudioCaptureRequestedAndAllowed(); } bool MediaStreamDevicesController::HasVideo() const { return IsDeviceVideoCaptureRequestedAndAllowed(); } const std::string& MediaStreamDevicesController::GetSecurityOriginSpec() const { return request_.security_origin.spec(); } void MediaStreamDevicesController::Accept(bool update_content_setting) { NotifyUIRequestAccepted(); // Get the default devices for the request. content::MediaStreamDevices devices; bool audio_allowed = IsDeviceAudioCaptureRequestedAndAllowed(); bool video_allowed = IsDeviceVideoCaptureRequestedAndAllowed(); if (audio_allowed || video_allowed) { switch (request_.request_type) { case content::MEDIA_OPEN_DEVICE: { const content::MediaStreamDevice* device = NULL; // For open device request, when requested device_id is empty, pick // the first available of the given type. If requested device_id is // not empty, return the desired device if it's available. Otherwise, // return no device. if (audio_allowed && request_.audio_type == content::MEDIA_DEVICE_AUDIO_CAPTURE) { if (!request_.requested_audio_device_id.empty()) { device = MediaCaptureDevicesDispatcher::GetInstance()-> GetRequestedAudioDevice(request_.requested_audio_device_id); } else { device = MediaCaptureDevicesDispatcher::GetInstance()-> GetFirstAvailableAudioDevice(); } } else if (video_allowed && request_.video_type == content::MEDIA_DEVICE_VIDEO_CAPTURE) { // Pepper API opens only one device at a time. if (!request_.requested_video_device_id.empty()) { device = MediaCaptureDevicesDispatcher::GetInstance()-> GetRequestedVideoDevice(request_.requested_video_device_id); } else { device = MediaCaptureDevicesDispatcher::GetInstance()-> GetFirstAvailableVideoDevice(); } } if (device) devices.push_back(*device); break; } case content::MEDIA_GENERATE_STREAM: { bool get_default_audio_device = audio_allowed; bool get_default_video_device = video_allowed; // Get the exact audio or video device if an id is specified. if (audio_allowed && !request_.requested_audio_device_id.empty()) { const content::MediaStreamDevice* audio_device = MediaCaptureDevicesDispatcher::GetInstance()-> GetRequestedAudioDevice(request_.requested_audio_device_id); if (audio_device) { devices.push_back(*audio_device); get_default_audio_device = false; } } if (video_allowed && !request_.requested_video_device_id.empty()) { const content::MediaStreamDevice* video_device = MediaCaptureDevicesDispatcher::GetInstance()-> GetRequestedVideoDevice(request_.requested_video_device_id); if (video_device) { devices.push_back(*video_device); get_default_video_device = false; } } // If either or both audio and video devices were requested but not // specified by id, get the default devices. if (get_default_audio_device || get_default_video_device) { MediaCaptureDevicesDispatcher::GetInstance()-> GetDefaultDevicesForProfile(profile_, get_default_audio_device, get_default_video_device, &devices); } break; } case content::MEDIA_DEVICE_ACCESS: { // Get the default devices for the request. MediaCaptureDevicesDispatcher::GetInstance()-> GetDefaultDevicesForProfile(profile_, audio_allowed, video_allowed, &devices); break; } case content::MEDIA_ENUMERATE_DEVICES: { // Do nothing. NOTREACHED(); break; } } // switch // TODO(raymes): We currently set the content permission for non-https // websites for Pepper requests as well. This is temporary and should be // removed. if (update_content_setting) { if ((IsSchemeSecure() && !devices.empty()) || request_.request_type == content::MEDIA_OPEN_DEVICE) { SetPermission(true); } } if (audio_allowed) { profile_->GetHostContentSettingsMap()->UpdateLastUsageByPattern( ContentSettingsPattern::FromURLNoWildcard(request_.security_origin), ContentSettingsPattern::Wildcard(), CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC); } if (video_allowed) { profile_->GetHostContentSettingsMap()->UpdateLastUsageByPattern( ContentSettingsPattern::FromURLNoWildcard(request_.security_origin), ContentSettingsPattern::Wildcard(), CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA); } } scoped_ptr ui; if (!devices.empty()) { ui = MediaCaptureDevicesDispatcher::GetInstance()-> GetMediaStreamCaptureIndicator()->RegisterMediaStream( web_contents_, devices); } content::MediaResponseCallback cb = callback_; callback_.Reset(); cb.Run(devices, devices.empty() ? content::MEDIA_DEVICE_NO_HARDWARE : content::MEDIA_DEVICE_OK, ui.Pass()); } void MediaStreamDevicesController::Deny( bool update_content_setting, content::MediaStreamRequestResult result) { DLOG(WARNING) << "MediaStreamDevicesController::Deny: " << result; NotifyUIRequestDenied(); if (update_content_setting) { CHECK_EQ(content::MEDIA_DEVICE_PERMISSION_DENIED, result); SetPermission(false); } content::MediaResponseCallback cb = callback_; callback_.Reset(); cb.Run(content::MediaStreamDevices(), result, scoped_ptr()); } int MediaStreamDevicesController::GetIconID() const { if (HasVideo()) return IDR_INFOBAR_MEDIA_STREAM_CAMERA; return IDR_INFOBAR_MEDIA_STREAM_MIC; } base::string16 MediaStreamDevicesController::GetMessageText() const { int message_id = IDS_MEDIA_CAPTURE_AUDIO_AND_VIDEO; if (!HasAudio()) message_id = IDS_MEDIA_CAPTURE_VIDEO_ONLY; else if (!HasVideo()) message_id = IDS_MEDIA_CAPTURE_AUDIO_ONLY; return l10n_util::GetStringFUTF16( message_id, base::UTF8ToUTF16(GetSecurityOriginSpec())); } base::string16 MediaStreamDevicesController::GetMessageTextFragment() const { int message_id = IDS_MEDIA_CAPTURE_AUDIO_AND_VIDEO_PERMISSION_FRAGMENT; if (!HasAudio()) message_id = IDS_MEDIA_CAPTURE_VIDEO_ONLY_PERMISSION_FRAGMENT; else if (!HasVideo()) message_id = IDS_MEDIA_CAPTURE_AUDIO_ONLY_PERMISSION_FRAGMENT; return l10n_util::GetStringUTF16(message_id); } bool MediaStreamDevicesController::HasUserGesture() const { return request_.user_gesture; } GURL MediaStreamDevicesController::GetRequestingHostname() const { return request_.security_origin; } void MediaStreamDevicesController::PermissionGranted() { GURL origin(GetSecurityOriginSpec()); if (origin.SchemeIsSecure()) { UMA_HISTOGRAM_ENUMERATION("Media.DevicePermissionActions", kAllowHttps, kPermissionActionsMax); } else { UMA_HISTOGRAM_ENUMERATION("Media.DevicePermissionActions", kAllowHttp, kPermissionActionsMax); } Accept(true); } void MediaStreamDevicesController::PermissionDenied() { UMA_HISTOGRAM_ENUMERATION("Media.DevicePermissionActions", kDeny, kPermissionActionsMax); Deny(true, content::MEDIA_DEVICE_PERMISSION_DENIED); } void MediaStreamDevicesController::Cancelled() { UMA_HISTOGRAM_ENUMERATION("Media.DevicePermissionActions", kCancel, kPermissionActionsMax); Deny(false, content::MEDIA_DEVICE_PERMISSION_DISMISSED); } void MediaStreamDevicesController::RequestFinished() { delete this; } bool MediaStreamDevicesController::IsRequestAllowedByDefault() const { // The request from internal objects like chrome://URLs is always allowed. if (CheckAllowAllMediaStreamContentForOrigin(profile_, request_.security_origin)) { return true; } struct { bool has_capability; const char* policy_name; const char* list_policy_name; ContentSettingsType settings_type; } device_checks[] = { { IsDeviceAudioCaptureRequestedAndAllowed(), prefs::kAudioCaptureAllowed, prefs::kAudioCaptureAllowedUrls, CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC }, { IsDeviceVideoCaptureRequestedAndAllowed(), prefs::kVideoCaptureAllowed, prefs::kVideoCaptureAllowedUrls, CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA }, }; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(device_checks); ++i) { if (!device_checks[i].has_capability) continue; MediaStreamDevicePolicy policy = GetDevicePolicy(profile_, request_.security_origin, device_checks[i].policy_name, device_checks[i].list_policy_name); if (policy == ALWAYS_DENY) return false; if (policy == POLICY_NOT_SET) { // Only load content settings from secure origins unless it is a // content::MEDIA_OPEN_DEVICE (Pepper) request. if (!IsSchemeSecure() && request_.request_type != content::MEDIA_OPEN_DEVICE) { return false; } if (profile_->GetHostContentSettingsMap()->GetContentSetting( request_.security_origin, request_.security_origin, device_checks[i].settings_type, NO_RESOURCE_IDENTIFIER) != CONTENT_SETTING_ALLOW) { return false; } } // If we get here, then either policy is set to ALWAYS_ALLOW or the content // settings allow the request by default. } return true; } int MediaStreamDevicesController::FilterBlockedByDefaultDevices() { int requested_devices = 0; if (IsDeviceAudioCaptureRequestedAndAllowed()) { if (profile_->GetHostContentSettingsMap()->GetContentSetting( request_.security_origin, request_.security_origin, CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC, NO_RESOURCE_IDENTIFIER) == CONTENT_SETTING_BLOCK) { request_permissions_[content::MEDIA_DEVICE_AUDIO_CAPTURE].permission = MEDIA_BLOCKED_BY_USER_SETTING; } else { ++requested_devices; } } if (IsDeviceVideoCaptureRequestedAndAllowed()) { if (profile_->GetHostContentSettingsMap()->GetContentSetting( request_.security_origin, request_.security_origin, CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA, NO_RESOURCE_IDENTIFIER) == CONTENT_SETTING_BLOCK) { request_permissions_[content::MEDIA_DEVICE_VIDEO_CAPTURE].permission = MEDIA_BLOCKED_BY_USER_SETTING; } else { ++requested_devices; } } return requested_devices; } bool MediaStreamDevicesController::IsDefaultMediaAccessBlocked() const { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); // TODO(markusheintz): Replace CONTENT_SETTINGS_TYPE_MEDIA_STREAM with the // appropriate new CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC and // CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA. ContentSetting current_setting = profile_->GetHostContentSettingsMap()->GetDefaultContentSetting( CONTENT_SETTINGS_TYPE_MEDIASTREAM, NULL); return (current_setting == CONTENT_SETTING_BLOCK); } bool MediaStreamDevicesController::IsSchemeSecure() const { return request_.security_origin.SchemeIsSecure() || request_.security_origin.SchemeIs(extensions::kExtensionScheme); } void MediaStreamDevicesController::SetPermission(bool allowed) const { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); ContentSettingsPattern primary_pattern = ContentSettingsPattern::FromURLNoWildcard(request_.security_origin); // Check the pattern is valid or not. When the request is from a file access, // no exception will be made. if (!primary_pattern.IsValid()) return; ContentSetting content_setting = allowed ? CONTENT_SETTING_ALLOW : CONTENT_SETTING_BLOCK; if (request_permissions_.find(content::MEDIA_DEVICE_AUDIO_CAPTURE) != request_permissions_.end()) { profile_->GetHostContentSettingsMap()->SetContentSetting( primary_pattern, ContentSettingsPattern::Wildcard(), CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC, std::string(), content_setting); } if (request_permissions_.find(content::MEDIA_DEVICE_VIDEO_CAPTURE) != request_permissions_.end()) { profile_->GetHostContentSettingsMap()->SetContentSetting( primary_pattern, ContentSettingsPattern::Wildcard(), CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA, std::string(), content_setting); } } void MediaStreamDevicesController::NotifyUIRequestAccepted() const { if (!content_settings_) return; content_settings_->OnMediaStreamPermissionSet(request_.security_origin, request_permissions_); } void MediaStreamDevicesController::NotifyUIRequestDenied() { if (!content_settings_) return; if (IsDeviceAudioCaptureRequestedAndAllowed()) { request_permissions_[content::MEDIA_DEVICE_AUDIO_CAPTURE].permission = MEDIA_BLOCKED_BY_USER; } if (IsDeviceVideoCaptureRequestedAndAllowed()) { request_permissions_[content::MEDIA_DEVICE_VIDEO_CAPTURE].permission = MEDIA_BLOCKED_BY_USER; } content_settings_->OnMediaStreamPermissionSet(request_.security_origin, request_permissions_); } bool MediaStreamDevicesController::IsDeviceAudioCaptureRequestedAndAllowed() const { MediaStreamTypeSettingsMap::const_iterator it = request_permissions_.find(content::MEDIA_DEVICE_AUDIO_CAPTURE); return (it != request_permissions_.end() && IsCaptureDeviceRequestAllowed() && it->second.permission == MEDIA_ALLOWED); } bool MediaStreamDevicesController::IsDeviceVideoCaptureRequestedAndAllowed() const { MediaStreamTypeSettingsMap::const_iterator it = request_permissions_.find(content::MEDIA_DEVICE_VIDEO_CAPTURE); return (it != request_permissions_.end() && IsCaptureDeviceRequestAllowed() && it->second.permission == MEDIA_ALLOWED); } bool MediaStreamDevicesController::IsCaptureDeviceRequestAllowed() const { #if defined(OS_ANDROID) // Don't approve device requests if the tab was hidden. // TODO(qinmin): Add a test for this. http://crbug.com/396869. return web_contents_->GetRenderWidgetHostView()->IsShowing(); #endif return true; }