Require <iframe allow="display"> for an iframe to use getDisplayMedia
authoreric.carlson@apple.com <eric.carlson@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 12 Nov 2018 17:18:48 +0000 (17:18 +0000)
committereric.carlson@apple.com <eric.carlson@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 12 Nov 2018 17:18:48 +0000 (17:18 +0000)
https://bugs.webkit.org/show_bug.cgi?id=191505
<rdar://problem/45968811>

Reviewed by Jer Noble.

LayoutTests/imported/w3c:

* web-platform-tests/mediacapture-streams/MediaStream-default-feature-policy.https-expected.txt:

Source/WebCore:

Test: http/tests/media/media-stream/get-display-media-iframe-allow-attribute.html

* Modules/mediastream/MediaDevicesRequest.cpp:
(WebCore::MediaDevicesRequest::start):
* Modules/mediastream/UserMediaController.cpp:
(WebCore::isAllowedToUse):
(WebCore::UserMediaController::canCallGetUserMedia):
(WebCore::UserMediaController::logGetUserMediaDenial):
* Modules/mediastream/UserMediaController.h:
* Modules/mediastream/UserMediaRequest.cpp:
(WebCore::UserMediaRequest::start):

LayoutTests:

* http/tests/media/media-stream/enumerate-devices-iframe-allow-attribute-expected.txt:
* http/tests/media/media-stream/get-display-media-iframe-allow-attribute-expected.txt: Added.
* http/tests/media/media-stream/get-display-media-iframe-allow-attribute.html: Added.
* http/tests/media/media-stream/resources/get-display-media-devices-iframe.html: Added.
* http/tests/ssl/media-stream/get-user-media-different-host-expected.txt:
* http/tests/ssl/media-stream/get-user-media-nested-expected.txt:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@238091 268f45cc-cd09-0410-ab3c-d52691b4dbfc

14 files changed:
LayoutTests/ChangeLog
LayoutTests/http/tests/media/media-stream/enumerate-devices-iframe-allow-attribute-expected.txt
LayoutTests/http/tests/media/media-stream/get-display-media-iframe-allow-attribute-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/media/media-stream/get-display-media-iframe-allow-attribute.html [new file with mode: 0644]
LayoutTests/http/tests/media/media-stream/resources/get-display-media-devices-iframe.html [new file with mode: 0644]
LayoutTests/http/tests/ssl/media-stream/get-user-media-different-host-expected.txt
LayoutTests/http/tests/ssl/media-stream/get-user-media-nested-expected.txt
LayoutTests/imported/w3c/ChangeLog
LayoutTests/imported/w3c/web-platform-tests/mediacapture-streams/MediaStream-default-feature-policy.https-expected.txt
Source/WebCore/ChangeLog
Source/WebCore/Modules/mediastream/MediaDevicesRequest.cpp
Source/WebCore/Modules/mediastream/UserMediaController.cpp
Source/WebCore/Modules/mediastream/UserMediaController.h
Source/WebCore/Modules/mediastream/UserMediaRequest.cpp

index 2373204..298fa83 100644 (file)
@@ -1,3 +1,18 @@
+2018-11-12  Eric Carlson  <eric.carlson@apple.com>
+
+        Require <iframe allow="display"> for an iframe to use getDisplayMedia
+        https://bugs.webkit.org/show_bug.cgi?id=191505
+        <rdar://problem/45968811>
+
+        Reviewed by Jer Noble.
+
+        * http/tests/media/media-stream/enumerate-devices-iframe-allow-attribute-expected.txt:
+        * http/tests/media/media-stream/get-display-media-iframe-allow-attribute-expected.txt: Added.
+        * http/tests/media/media-stream/get-display-media-iframe-allow-attribute.html: Added.
+        * http/tests/media/media-stream/resources/get-display-media-devices-iframe.html: Added.
+        * http/tests/ssl/media-stream/get-user-media-different-host-expected.txt:
+        * http/tests/ssl/media-stream/get-user-media-nested-expected.txt:
+
 2018-11-12  Simon Fraser  <simon.fraser@apple.com>
 
         Make compositing updates incremental
index 48986bb..5b7b75e 100644 (file)
@@ -1,3 +1,3 @@
-CONSOLE MESSAGE: line 4: The top-level frame has prevented a document with a different security origin from calling enumerateDevices.
+CONSOLE MESSAGE: line 4: Trying to call enumerateDevices from a frame without correct 'allow' attribute.
       
 PASS: "allow" attribute respected in all iframes
diff --git a/LayoutTests/http/tests/media/media-stream/get-display-media-iframe-allow-attribute-expected.txt b/LayoutTests/http/tests/media/media-stream/get-display-media-iframe-allow-attribute-expected.txt
new file mode 100644 (file)
index 0000000..03745af
--- /dev/null
@@ -0,0 +1,9 @@
+CONSOLE MESSAGE: line 5: Trying to call getDisplayMedia from a frame without correct 'allow' attribute.
+CONSOLE MESSAGE: line 5: Trying to call getDisplayMedia from a frame without correct 'allow' attribute.
+CONSOLE MESSAGE: line 5: Trying to call getDisplayMedia from a frame without correct 'allow' attribute.
+     
+
+PASS: <iframe allow=''> got "deny"
+PASS: <iframe allow='camera'> got "deny"
+PASS: <iframe allow='display'> got "allow"
+PASS: <iframe allow='microphone'> got "deny"
diff --git a/LayoutTests/http/tests/media/media-stream/get-display-media-iframe-allow-attribute.html b/LayoutTests/http/tests/media/media-stream/get-display-media-iframe-allow-attribute.html
new file mode 100644 (file)
index 0000000..ac32b87
--- /dev/null
@@ -0,0 +1,59 @@
+<!doctype html>
+<html>
+<script src="../../../../resources/js-test-pre.js"></script>
+<body>
+    <iframe id="none" src="http://localhost:8000/media/media-stream/resources/get-display-media-devices-iframe.html"></iframe>
+    <iframe id="camera" allow="camera" src="http://localhost:8000/media/media-stream/resources/get-display-media-devices-iframe.html"></iframe>
+    <iframe id="microphone" allow="microphone" src="http://localhost:8000/media/media-stream/resources/get-display-media-devices-iframe.html"></iframe>
+    <iframe id="display" allow="display" src="http://localhost:8000/media/media-stream/resources/get-display-media-devices-iframe.html"></iframe>
+
+    <script>
+        if (window.testRunner) {
+            testRunner.dumpAsText();
+            testRunner.waitUntilDone();
+            testRunner.setUserMediaPermission(true);
+        }
+        if (window.internals)
+            window.internals.settings.setScreenCaptureEnabled(true);
+
+        let results = [];
+        let writeResults = () => {
+            result.innerHTML = '<br>' + results.sort().join('<br>');
+        }
+
+        handle = setTimeout(() => {
+            testFailed('Timeout: not all frames loaded correctly.');
+            writeResults();
+            if (window.testRunner)
+                testRunner.notifyDone();
+        }, 4000);
+
+        let expected = {
+            'none' : 'deny',
+            'camera' : 'deny',
+            'microphone' : 'deny',
+            'display' : 'allow',
+        };
+        let success = true;
+        window.addEventListener("message", (event) => {
+            let message = event.data.trim().split(':');
+            let iframe = document.getElementById(message[0]);
+            let allow = `'${iframe.allow || ""}'`;
+            let success = expected[message[0]] == message[1];
+            results.push(`${success ? "PASS" : "FAIL"}: &LT;iframe allow=${allow}> got "${message[1]}"${success ? "" : ", should have ${expected[message[0]]}"}`);
+            if (results.length == 4) {
+                clearTimeout(handle);
+                writeResults();
+                if (window.testRunner)
+                    testRunner.notifyDone();
+            }
+            
+        }, false);
+
+        let trigerTest = (evt) => { evt.target.contentWindow.postMessage(evt.target.id, '*'); }
+        Array.from(document.getElementsByTagName('iframe')).forEach(element => element.onload = trigerTest);
+    </script>
+
+    <div id="result"></div>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/media/media-stream/resources/get-display-media-devices-iframe.html b/LayoutTests/http/tests/media/media-stream/resources/get-display-media-devices-iframe.html
new file mode 100644 (file)
index 0000000..c0f4508
--- /dev/null
@@ -0,0 +1,17 @@
+<script>
+    async function enumerate(event)
+    {
+        let result;
+        await navigator.mediaDevices.getDisplayMedia({video: true})
+            .then((s) => result = "allow")
+            .catch((e) => result = "deny");
+        parent.postMessage(`${event.data}:${result}`, '*');
+        result.innerHTML = result;
+    }
+
+    window.addEventListener("message", (id) => enumerate(id));
+</script>
+
+<div id='result'></div>
+
+
index 356f1d6..63974b6 100644 (file)
@@ -1,4 +1,4 @@
-CONSOLE MESSAGE: line 52: The top-level frame has prevented a document with a different security origin from calling getUserMedia.
+CONSOLE MESSAGE: line 52: Trying to call getUserMedia from a frame without correct 'allow' attribute.
 Tests that getUserMedia fails when the top level document and iframe do not have the same domain.
 
 On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
index ab8fb7c..b408099 100644 (file)
@@ -1,4 +1,4 @@
-CONSOLE MESSAGE: line 52: The top-level frame has prevented a document with a different security origin from calling getUserMedia.
+CONSOLE MESSAGE: line 52: Trying to call getUserMedia from a frame without correct 'allow' attribute.
 Tests that getUserMedia fails when the top level document and iframe do not have the same domain.
 
 On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
index 447f1c3..173676f 100644 (file)
@@ -1,3 +1,13 @@
+2018-11-12  Eric Carlson  <eric.carlson@apple.com>
+
+        Require <iframe allow="display"> for an iframe to use getDisplayMedia
+        https://bugs.webkit.org/show_bug.cgi?id=191505
+        <rdar://problem/45968811>
+
+        Reviewed by Jer Noble.
+
+        * web-platform-tests/mediacapture-streams/MediaStream-default-feature-policy.https-expected.txt:
+
 2018-11-11  Javier Fernandez  <jfernandez@igalia.com>
 
         [css-grid] Import additional grid layout test from the WPT suite
index b1aaa6c..3cc7066 100644 (file)
@@ -1,10 +1,10 @@
-CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin from calling getUserMedia.
-CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin from calling getUserMedia.
-CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin from calling getUserMedia.
-CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin from calling getUserMedia.
-CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin from calling getUserMedia.
-CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin from calling getUserMedia.
-CONSOLE MESSAGE: line 16: The top-level frame has prevented a document with a different security origin from calling getUserMedia.
+CONSOLE MESSAGE: line 16: Trying to call getUserMedia from a frame without correct 'allow' attribute.
+CONSOLE MESSAGE: line 16: Trying to call getUserMedia from a frame without correct 'allow' attribute.
+CONSOLE MESSAGE: line 16: Trying to call getUserMedia from a frame without correct 'allow' attribute.
+CONSOLE MESSAGE: line 16: Trying to call getUserMedia from a frame without correct 'allow' attribute.
+CONSOLE MESSAGE: line 16: Trying to call getUserMedia from a frame without correct 'allow' attribute.
+CONSOLE MESSAGE: line 16: Trying to call getUserMedia from a frame without correct 'allow' attribute.
+CONSOLE MESSAGE: line 16: Trying to call getUserMedia from a frame without correct 'allow' attribute.
 
 
 PASS Default "microphone" feature policy ["self"] allows the top-level document. 
index 9b66987..b63e170 100644 (file)
@@ -1,3 +1,23 @@
+2018-11-12  Eric Carlson  <eric.carlson@apple.com>
+
+        Require <iframe allow="display"> for an iframe to use getDisplayMedia
+        https://bugs.webkit.org/show_bug.cgi?id=191505
+        <rdar://problem/45968811>
+
+        Reviewed by Jer Noble.
+
+        Test: http/tests/media/media-stream/get-display-media-iframe-allow-attribute.html
+
+        * Modules/mediastream/MediaDevicesRequest.cpp:
+        (WebCore::MediaDevicesRequest::start):
+        * Modules/mediastream/UserMediaController.cpp:
+        (WebCore::isAllowedToUse):
+        (WebCore::UserMediaController::canCallGetUserMedia):
+        (WebCore::UserMediaController::logGetUserMediaDenial):
+        * Modules/mediastream/UserMediaController.h:
+        * Modules/mediastream/UserMediaRequest.cpp:
+        (WebCore::UserMediaRequest::start):
+
 2018-11-12  Simon Fraser  <simon.fraser@apple.com>
 
         Make compositing updates incremental
index 37b288f..dba7a61 100644 (file)
@@ -113,8 +113,8 @@ void MediaDevicesRequest::start()
         return;
     }
 
-    auto microphoneAccess = controller->canCallGetUserMedia(document, true, false);
-    auto cameraAccess = controller->canCallGetUserMedia(document, false, true);
+    auto microphoneAccess = controller->canCallGetUserMedia(document, { UserMediaController::CaptureType::Microphone });
+    auto cameraAccess = controller->canCallGetUserMedia(document, { UserMediaController::CaptureType::Camera });
     bool canAccessMicrophone = microphoneAccess == UserMediaController::GetUserMediaAccess::CanCall;
     bool canAccessCamera = cameraAccess == UserMediaController::GetUserMediaAccess::CanCall;
     if (!canAccessMicrophone && !canAccessCamera) {
index 401b34a..6251a7e 100644 (file)
@@ -70,42 +70,48 @@ static bool isSecure(DocumentLoader& documentLoader)
         && !response.certificateInfo()->containsNonRootSHA1SignedCertificate();
 }
 
-static bool isAllowedToUse(Document& document, Document& topDocument, bool requiresAudio, bool requiresVideo)
+static UserMediaController::GetUserMediaAccess isAllowedToUse(Document& document, Document& topDocument, OptionSet<UserMediaController::CaptureType> types)
 {
     if (&document == &topDocument)
-        return true;
+        return UserMediaController::GetUserMediaAccess::CanCall;
 
     auto* parentDocument = document.parentDocument();
     if (!parentDocument)
-        return false;
+        return UserMediaController::GetUserMediaAccess::BlockedByParent;
 
     if (document.securityOrigin().isSameSchemeHostPort(parentDocument->securityOrigin()))
-        return true;
+        return UserMediaController::GetUserMediaAccess::CanCall;
 
     auto* element = document.ownerElement();
     ASSERT(element);
     if (!element)
-        return false;
+        return UserMediaController::GetUserMediaAccess::BlockedByParent;
 
     if (!is<HTMLIFrameElement>(*element))
-        return false;
+        return UserMediaController::GetUserMediaAccess::BlockedByParent;
     auto& allow = downcast<HTMLIFrameElement>(*element).allow();
 
     bool allowCameraAccess = false;
     bool allowMicrophoneAccess = false;
+    bool allowDisplay = false;
     for (auto allowItem : StringView { allow }.split(';')) {
         auto item = allowItem.stripLeadingAndTrailingMatchedCharacters(isHTMLSpace<UChar>);
         if (!allowCameraAccess && item == "camera")
             allowCameraAccess = true;
         else if (!allowMicrophoneAccess && item == "microphone")
             allowMicrophoneAccess = true;
+        else if (!allowDisplay && item == "display")
+            allowDisplay = true;
     }
-    return (allowCameraAccess || !requiresVideo) && (allowMicrophoneAccess || !requiresAudio);
+    if ((allowCameraAccess || !(types & UserMediaController::CaptureType::Camera)) && (allowMicrophoneAccess || !(types & UserMediaController::CaptureType::Microphone)) && (allowDisplay || !(types & UserMediaController::CaptureType::Display)))
+        return UserMediaController::GetUserMediaAccess::CanCall;
+
+    return UserMediaController::GetUserMediaAccess::BlockedByFeaturePolicy;
 }
 
-UserMediaController::GetUserMediaAccess UserMediaController::canCallGetUserMedia(Document& document, bool wantsAudio, bool wantsVideo)
+UserMediaController::GetUserMediaAccess UserMediaController::canCallGetUserMedia(Document& document, OptionSet<UserMediaController::CaptureType> types)
 {
-    ASSERT(wantsAudio || wantsVideo);
+    ASSERT(!types.isEmpty());
 
     bool requiresSecureConnection = DeprecatedGlobalSettings::mediaCaptureRequiresSecureConnection();
     auto& documentLoader = *document.loader();
@@ -118,8 +124,9 @@ UserMediaController::GetUserMediaAccess UserMediaController::canCallGetUserMedia
             if (requiresSecureConnection && !isSecure(*ancestorDocument->loader()))
                 return GetUserMediaAccess::InsecureParent;
 
-            if (!isAllowedToUse(*ancestorDocument, topDocument, wantsAudio, wantsVideo))
-                return GetUserMediaAccess::BlockedByParent;
+            auto status = isAllowedToUse(*ancestorDocument, topDocument, types);
+            if (status != GetUserMediaAccess::CanCall)
+                return status;
         }
     }
 
@@ -129,7 +136,20 @@ UserMediaController::GetUserMediaAccess UserMediaController::canCallGetUserMedia
 void UserMediaController::logGetUserMediaDenial(Document& document, GetUserMediaAccess access, BlockedCaller caller)
 {
     auto& domWindow = *document.domWindow();
-    const char* callerName = caller == BlockedCaller::GetUserMedia ? "getUserMedia" : "enumerateDevices";
+    const char* callerName;
+
+    switch (caller) {
+    case BlockedCaller::GetUserMedia:
+        callerName = "getUserMedia";
+        break;
+    case BlockedCaller::GetDisplayMedia:
+        callerName = "getDisplayMedia";
+        break;
+    case BlockedCaller::EnumerateDevices:
+        callerName = "enumerateDevices";
+        break;
+    }
+
     switch (access) {
     case UserMediaController::GetUserMediaAccess::InsecureDocument:
         domWindow.printErrorMessage(makeString("Trying to call ", callerName, " from an insecure document."));
@@ -140,6 +160,9 @@ void UserMediaController::logGetUserMediaDenial(Document& document, GetUserMedia
     case UserMediaController::GetUserMediaAccess::BlockedByParent:
         domWindow.printErrorMessage(makeString("The top-level frame has prevented a document with a different security origin from calling ", callerName, "."));
         break;
+    case GetUserMediaAccess::BlockedByFeaturePolicy:
+        domWindow.printErrorMessage(makeString("Trying to call ", callerName, " from a frame without correct 'allow' attribute."));
+        break;
     case UserMediaController::GetUserMediaAccess::CanCall:
         break;
     }
index eac75b9..dfe8969 100644 (file)
@@ -55,13 +55,20 @@ public:
         CanCall,
         InsecureDocument,
         InsecureParent,
-        BlockedByParent
+        BlockedByParent,
+        BlockedByFeaturePolicy,
     };
-    GetUserMediaAccess canCallGetUserMedia(Document&, bool wantsAudio, bool wantsVideo);
+    enum class CaptureType {
+        Microphone = 1 << 0,
+        Camera = 1 << 1,
+        Display = 1 << 3
+    };
+    GetUserMediaAccess canCallGetUserMedia(Document&, OptionSet<CaptureType>);
 
     enum class BlockedCaller {
         GetUserMedia,
-        EnumerateDevices
+        GetDisplayMedia,
+        EnumerateDevices,
     };
     void logGetUserMediaDenial(Document&, GetUserMediaAccess, BlockedCaller);
 
index 3bae11d..f616eb7 100644 (file)
@@ -189,10 +189,23 @@ void UserMediaRequest::start()
     // ...
     // 6.10 Permission Failure: Reject p with a new DOMException object whose name attribute has
     //      the value NotAllowedError.
-    auto access = controller->canCallGetUserMedia(document, m_request.audioConstraints.isValid, m_request.videoConstraints.isValid);
+
+    OptionSet<UserMediaController::CaptureType> types;
+    UserMediaController::BlockedCaller caller;
+    if (m_request.type == MediaStreamRequest::Type::DisplayMedia) {
+        types.add(UserMediaController::CaptureType::Display);
+        caller = UserMediaController::BlockedCaller::GetDisplayMedia;
+    } else {
+        if (m_request.audioConstraints.isValid)
+            types.add(UserMediaController::CaptureType::Microphone);
+        if (m_request.videoConstraints.isValid)
+            types.add(UserMediaController::CaptureType::Camera);
+        caller = UserMediaController::BlockedCaller::GetUserMedia;
+    }
+    auto access = controller->canCallGetUserMedia(document, types);
     if (access != UserMediaController::GetUserMediaAccess::CanCall) {
         deny(MediaAccessDenialReason::PermissionDenied);
-        controller->logGetUserMediaDenial(document, access, UserMediaController::BlockedCaller::GetUserMedia);
+        controller->logGetUserMediaDenial(document, access, caller);
         return;
     }