Browse Source

Only use CSP workaround when actually needed.

Ian Walton 1 year ago
parent
commit
613773fd1e

+ 62 - 26
native/find-webclient.js

@@ -1,3 +1,24 @@
+function loadScript(src) {
+    return new Promise((resolve, reject) => {
+        const s = document.createElement('script');
+        s.src = src;
+        s.onload = resolve;
+        s.onerror = reject;
+        document.head.appendChild(s);
+    });
+}
+
+async function createApi() {
+    await loadScript('qrc:///qtwebchannel/qwebchannel.js');
+    const channel = await new Promise((resolve) => {
+        /*global QWebChannel */
+        new QWebChannel(window.qt.webChannelTransport, resolve);
+    });
+    return channel.objects;
+}
+
+window.apiPromise = createApi();
+
 async function tryConnect(server) {
     document.getElementById('connect-button').disabled = true;
 
@@ -14,36 +35,47 @@ async function tryConnect(server) {
                 throw new Error("Status not ok");
             }
 
-            // Sigh... If we just navigate to the URL, the server's CSP will block us loading other resources.
-            // So we have to parse the HTML, set a new base href, and then write it back to the page.
-            // We also have to override the history functions to make sure they use the correct URL.
-            const webUrl = htmlResponse.url.replace(/\/[^\/]*$/, "/");
-            const realUrl = window.location.href;
+            if (response.headers.get("content-security-policy")) {
+                // Sigh... If we just navigate to the URL, the server's CSP will block us loading other resources.
+                // So we have to parse the HTML, set a new base href, and then write it back to the page.
+                // We also have to override the history functions to make sure they use the correct URL.
+                console.log("Using CSP workaround");
+                const webUrl = htmlResponse.url.replace(/\/[^\/]*$/, "/");
+                const realUrl = window.location.href;
 
-            const html = await htmlResponse.text();
-            const parser = new DOMParser();
-            const doc = parser.parseFromString(html, "text/html");
-            const base = doc.createElement("base");
-            base.href = webUrl
-            doc.head.insertBefore(base, doc.head.firstChild);
-            
-            const oldPushState = window.history.pushState;
-            window.history.pushState = function(state, title, url) {
-                url = (new URL(url, realUrl)).toString();
-                return oldPushState.call(window.history, state, title, url);
-            };
+                const html = await htmlResponse.text();
+                const parser = new DOMParser();
+                const doc = parser.parseFromString(html, "text/html");
+                const base = doc.createElement("base");
+                base.href = webUrl
+                doc.head.insertBefore(base, doc.head.firstChild);
+                
+                const oldPushState = window.history.pushState;
+                window.history.pushState = function(state, title, url) {
+                    url = (new URL(url, realUrl)).toString();
+                    return oldPushState.call(window.history, state, title, url);
+                };
 
-            const oldReplaceState = window.history.replaceState;
-            window.history.replaceState = function(state, title, url) {
-                url = (new URL(url, realUrl)).toString();
-                return oldReplaceState.call(window.history, state, title, url);
-            };
+                const oldReplaceState = window.history.replaceState;
+                window.history.replaceState = function(state, title, url) {
+                    url = (new URL(url, realUrl)).toString();
+                    return oldReplaceState.call(window.history, state, title, url);
+                };
 
-            document.open();
-            document.write((new XMLSerializer()).serializeToString(doc));
-            document.close();
+                document.open();
+                document.write((new XMLSerializer()).serializeToString(doc));
+                document.close();
+            } else {
+                console.log("Using normal navigation");
+                window.location = server;
+            }
 
             window.localStorage.setItem("saved-server", server);
+            
+            const api = await window.apiPromise;
+            await new Promise(resolve => {
+                api.settings.setValue('main', 'userWebClient', server, resolve);
+            });
             return true;
         }
     } catch (e) {
@@ -66,10 +98,14 @@ document.getElementById('connect-fail-button').addEventListener('click', () => {
     document.getElementById('backdrop').style.display = 'none';
 });
 
-const savedServer = window.localStorage.getItem("saved-server");
 
 // load the server if we have one
 (async() => {
+    const api = await window.apiPromise;
+    const savedServer = await new Promise(resolve => {
+        api.settings.value('main', 'userWebClient', resolve);
+    });
+
     if (!savedServer || !(await tryConnect(savedServer))) {
         document.getElementById('splash').style.display = 'none';
         document.getElementById('main').style.display = 'block';

+ 28 - 7
native/nativeshell.js

@@ -133,7 +133,23 @@ function getDeviceProfile() {
 }
 
 async function createApi() {
-    await loadScript('qrc:///qtwebchannel/qwebchannel.js');
+    try {
+        await loadScript('qrc:///qtwebchannel/qwebchannel.js');
+    } catch (e) {
+        // try clearing out any cached CSPs
+        let foundCache = false;
+        for (const cache of await caches.keys()) {
+            const dataDeleted = await caches.delete(cache);
+            if (dataDeleted) {
+                foundCache = true;
+            }
+        }
+        if (foundCache) {
+            window.location.reload();
+        }
+        throw e;
+    }
+
     const channel = await new Promise((resolve) => {
         /*global QWebChannel */
         new QWebChannel(window.qt.webChannelTransport, resolve);
@@ -291,7 +307,11 @@ async function showSettingsModal() {
         createSection();
     }
 
-    if (window.localStorage.getItem("saved-server") != null) {
+    const savedServer = await new Promise(resolve => {
+        window.api.settings.value('main', 'userWebClient', resolve);
+    });
+
+    if (savedServer) {
         const group = document.createElement("fieldset");
         group.className = "editItemMetadataForm editMetadataForm dialog-content-centered";
         group.style.border = 0;
@@ -303,7 +323,7 @@ async function showSettingsModal() {
         legend.appendChild(legendHeader);
         const legendSubHeader = document.createElement("h4");
         legendSubHeader.textContent = (
-            "The server you first connected to is your saved server. " + 
+            "The server you first connected to is your saved server. " +
             "It provides the web client for Jellyfin Media Player in the absence of a bundled one. " +
             "You can use this option to change it to another one. This does NOT log you off."
         );
@@ -316,10 +336,11 @@ async function showSettingsModal() {
         resetSavedServer.style.marginLeft = "auto";
         resetSavedServer.style.marginRight = "auto";
         resetSavedServer.style.maxWidth = "50%";
-        resetSavedServer.addEventListener("click", () => {
-            window.localStorage.removeItem("saved-server");
-            window.location.hash = "";
-            window.location.reload();
+        resetSavedServer.addEventListener("click", async () => {
+            await new Promise(resolve => {
+                window.api.settings.setValue('main', 'userWebClient', '', resolve);
+            });
+            window.location.href = viewdata.scriptPath + "/find-webclient.html";
         });
         group.appendChild(resetSavedServer);
     }

+ 5 - 0
resources/settings/settings_description.json

@@ -132,6 +132,11 @@
       {
         "value": "forceExternalWebclient",
         "default": false
+      },
+      {
+        "value": "userWebClient",
+        "default": "",
+        "hidden": true
       }
     ]
   },

+ 22 - 0
src/settings/SettingsComponent.cpp

@@ -717,6 +717,28 @@ bool SettingsComponent::resetAndSaveOldConfiguration()
   return settingsFile.rename(Paths::dataDir("jellyfinmediaplayer.conf.old"));
 }
 
+
+/////////////////////////////////////////////////////////////////////////////////////////
+bool SettingsComponent::isUsingExternalWebClient()
+{
+  QString url;
+
+  url = SettingsComponent::Get().value(SETTINGS_SECTION_PATH, "startupurl_desktop").toString();
+
+  if (url == "bundled")
+  {
+    auto path = Paths::webClientPath("desktop");
+    QFileInfo check_file(path);
+    if (SettingsComponent::Get().value(SETTINGS_SECTION_MAIN, "forceExternalWebclient").toBool() ||
+       !(check_file.exists() && check_file.isFile())) {
+      // use built-in fallback
+      return true;
+    }
+  }
+
+  return false;
+}
+
 /////////////////////////////////////////////////////////////////////////////////////////
 QString SettingsComponent::getWebClientUrl(bool desktop)
 {

+ 1 - 0
src/settings/SettingsComponent.h

@@ -59,6 +59,7 @@ public:
   Q_INVOKABLE QVariantList settingDescriptions();
   Q_INVOKABLE QString getWebClientUrl(bool desktop);
   Q_INVOKABLE QString getExtensionPath();
+  Q_INVOKABLE bool isUsingExternalWebClient();
   Q_INVOKABLE QString getClientName();
   Q_INVOKABLE bool ignoreSSLErrors();
 

+ 4 - 1
src/ui/webview.qml

@@ -1,6 +1,6 @@
 import QtQuick 2.4
 import Konvergo 1.0
-import QtWebEngine 1.1
+import QtWebEngine 1.7
 import QtWebChannel 1.0
 import QtQuick.Window 2.2
 import QtQuick.Controls 1.4
@@ -145,6 +145,9 @@ KonvergoWindow
     objectName: "web"
     settings.errorPageEnabled: false
     settings.localContentCanAccessRemoteUrls: true
+    settings.localContentCanAccessFileUrls: true
+    settings.allowRunningInsecureContent: components.settings.isUsingExternalWebClient()
+    settings.playbackRequiresUserGesture: false
     profile.httpUserAgent: components.system.getUserAgent()
     url: mainWindow.webUrl
     focus: true