Repository: manusa/electronim
Pull request #46 (2021-01-19 18:59:19)

🐞: Fix meet.google.com Webcam
There is a "hack" to allow google login which replaces the Electron/Chromium based
User-Agent with a fake FireFox user-agent.

This is causing trouble with the latest release of Google Meet.
The current fix replaces the logic for setting user-agent in this workaround and
adds a specific case to bypass meet.google.com

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=46&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=46&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=46&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=46&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=46&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=46&resolved=false&types=VULNERABILITY)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=46&resolved=false&types=SECURITY_HOTSPOT) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=46&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=46&resolved=false&types=SECURITY_HOTSPOT)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=46&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=46&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=46&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90.png' alt='96.9%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=46&metric=new_coverage&view=list) [96.9% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=46&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=46&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=46&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=46&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=46&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=46&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=46&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=46&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=46&resolved=false&types=VULNERABILITY)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=46&resolved=false&types=SECURITY_HOTSPOT) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=46&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=manusa_electronim&pullRequest=46&resolved=false&types=SECURITY_HOTSPOT)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=46&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=46&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=46&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90.png' alt='96.9%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=46&metric=new_coverage&view=list) [96.9% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=46&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=46&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=46&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/src/tab-manager/index.js b/src/tab-manager/index.js
index 099d84a..85ca3e4 100644
--- a/src/tab-manager/index.js
+++ b/src/tab-manager/index.js
@@ -17,7 +17,7 @@ const {app, BrowserView, Menu, MenuItem, session} = require('electron');
 const {APP_EVENTS} = require('../constants');
 const settings = require('../settings');
 const {contextMenuHandler} = require('../spell-check');
-const {userAgentForView} = require('../user-agent');
+const {userAgentForView, addUserAgentInterceptor} = require('../user-agent');
 const {handleRedirect} = require('./redirect');
 
 let activeTab = null;
@@ -67,8 +67,8 @@ const handleContextMenu = browserView => async (event, params) => {
 // Required for Service Workers -> https://github.com/electron/electron/issues/16196
 const setGlobalUserAgentFallback = userAgent => (app.userAgentFallback = userAgent);
 
-const cleanUserAgent = (browserView, url) => {
-  const validUserAgent = userAgentForView(browserView, url);
+const cleanUserAgent = browserView => {
+  const validUserAgent = userAgentForView(browserView);
   browserView.webContents.userAgent = validUserAgent;
   setGlobalUserAgentFallback(validUserAgent);
 };
@@ -78,11 +78,14 @@ const addTabs = ipcSender => tabsMetadata => {
     const tabPreferences = {...webPreferences};
     if (sandboxed) {
       tabPreferences.session = session.fromPartition(`persist:${id}`, {cache: true});
+    } else {
+      tabPreferences.session = session.defaultSession;
     }
+    addUserAgentInterceptor(tabPreferences.session);
     const tab = new BrowserView({webPreferences: tabPreferences});
     tab.setAutoResize({width: true, height: true});
 
-    cleanUserAgent(tab, url);
+    cleanUserAgent(tab);
     tab.webContents.loadURL(url);
 
     const handleRedirectForCurrentUrl = handleRedirect(tab);
diff --git a/src/user-agent/index.js b/src/user-agent/index.js
index 73e9512..8e43c42 100644
--- a/src/user-agent/index.js
+++ b/src/user-agent/index.js
@@ -19,7 +19,12 @@ const FIREFOX_VERSIONS = 'https://product-details.mozilla.org/1.0/firefox_versio
 
 const BROWSER_VERSIONS = {
   chromium: null,
-  firefox: null
+  firefox: null,
+  firefoxESR: null // Extended support release
+};
+
+const USER_AGENT_INTERCEPTOR_FILTER = {
+  urls: ['*://*.google.com/*']
 };
 
 const latestChromium = async () => {
@@ -36,6 +41,11 @@ const latestFirefox = async () => {
   return versions && versions.LATEST_FIREFOX_VERSION ? versions.LATEST_FIREFOX_VERSION : null;
 };
 
+const latestFirefoxESR = async () => {
+  const {data: versions} = await axios.get(FIREFOX_VERSIONS);
+  return versions && versions.FIREFOX_ESR ? versions.FIREFOX_ESR : null;
+};
+
 const initBrowserVersions = async () => {
   const setVersion = browser => version => {
     if (version) {
@@ -44,7 +54,8 @@ const initBrowserVersions = async () => {
   };
   await Promise.all([
     latestChromium().then(setVersion('chromium')),
-    latestFirefox().then(setVersion('firefox'))
+    latestFirefox().then(setVersion('firefox')),
+    latestFirefoxESR().then(setVersion('firefoxESR'))
   ]);
 };
 
@@ -58,24 +69,34 @@ const sanitizeUserAgent = userAgent => userAgent
 
 const defaultUserAgent = userAgent => sanitizeUserAgent(replaceChromeVersion(userAgent));
 
-const firefoxUserAgent = userAgent => {
-  if (BROWSER_VERSIONS.firefox) {
+const firefoxUserAgent = userAgent => firefoxVersion => {
+  if (firefoxVersion) {
     userAgent = userAgent.replace(/\) AppleWebKit.*/,
-      `; rv:${BROWSER_VERSIONS.firefox}) Gecko/20100101 Firefox/${BROWSER_VERSIONS.firefox}`);
+      `; rv:${firefoxVersion}) Gecko/20100101 Firefox/${firefoxVersion}`);
   }
   return userAgent;
 };
 
-const userAgentForView = (browserViewOrWindow, url = '') => {
-  let ret = defaultUserAgent(browserViewOrWindow.webContents.userAgent);
-  if (url.match(/https?:\/\/[^/]+google\.com.*/)) { // NOSONAR
-    ret = firefoxUserAgent(browserViewOrWindow.webContents.userAgent);
+const userAgentForView = browserViewOrWindow => defaultUserAgent(browserViewOrWindow.webContents.userAgent);
+
+const addUserAgentInterceptor = session => {
+  if (!session.userAgentInterceptor) {
+    session.webRequest.onBeforeSendHeaders(USER_AGENT_INTERCEPTOR_FILTER, (details, callback) => {
+      if (
+        details.url.match(/https?:\/\/[^/]+google\.com.*/) // NOSONAR
+        && !details.url.match(/https?:\/\/meet.google\.com.*/)
+      ) {
+        details.requestHeaders['User-Agent'] = firefoxUserAgent(details.requestHeaders['User-Agent'])(BROWSER_VERSIONS.firefoxESR);
+      }
+      callback({requestHeaders: details.requestHeaders});
+    });
+    session.userAgentInterceptor = true;
   }
-  return ret;
 };
 
 module.exports = {
   BROWSER_VERSIONS,
   initBrowserVersions,
-  userAgentForView
+  userAgentForView,
+  addUserAgentInterceptor
 };


Test patch:
diff --git a/src/tab-manager/__tests__/index.test.js b/src/tab-manager/__tests__/index.test.js
index 354f0d3..1db5ced 100644
--- a/src/tab-manager/__tests__/index.test.js
+++ b/src/tab-manager/__tests__/index.test.js
@@ -38,7 +38,12 @@ describe('Tab Manager module test suite', () => {
       BrowserView: jest.fn(() => mockBrowserView),
       Menu: jest.fn(() => mockMenu),
       MenuItem: jest.fn(() => mockMenuItem),
-      session: {fromPartition: jest.fn(() => ({}))}
+      session: {
+        fromPartition: jest.fn(() => ({
+          userAgentInterceptor: true
+        })),
+        defaultSession: {userAgentInterceptor: true}
+      }
     }));
     mockSettings = {
       loadSettings: jest.fn(() => ({
@@ -85,7 +90,7 @@ describe('Tab Manager module test suite', () => {
       // Then
       expect(require('electron').session.fromPartition).not.toHaveBeenCalled();
       expect(require('electron').BrowserView).toBeCalledWith({
-        webPreferences: expect.not.objectContaining({session: expect.anything()})});
+        webPreferences: expect.objectContaining({session: expect.anything()})});
     });
     test('sandboxed, should use isolated session', () => {
       // When
diff --git a/src/user-agent/__tests__/index.test.js b/src/user-agent/__tests__/index.test.js
index 5928ed2..49b5371 100644
--- a/src/user-agent/__tests__/index.test.js
+++ b/src/user-agent/__tests__/index.test.js
@@ -29,15 +29,17 @@ describe('User Agent module test suite', () => {
         {os: 'linux', versions: [{channel: 'other', version: '5uck5'}, {channel: 'stable', version: '1337'}]},
         {os: 'win'}
       ]}));
-      axios.get.mockImplementationOnce(async () => ({data: {
+      axios.get.mockImplementation(async () => ({data: {
         FIREFOX_DEVEDITION: '313373',
-        LATEST_FIREFOX_VERSION: 'ff.1337'
+        LATEST_FIREFOX_VERSION: 'ff.1337',
+        FIREFOX_ESR: 'ff.1337.esr'
       }}));
       // When
       await userAgent.initBrowserVersions();
       // Then
       expect(userAgent.BROWSER_VERSIONS.chromium).toBe('1337');
       expect(userAgent.BROWSER_VERSIONS.firefox).toBe('ff.1337');
+      expect(userAgent.BROWSER_VERSIONS.firefoxESR).toBe('ff.1337.esr');
     });
     test('invalidResponse, should return null,', async () => {
       // Given
@@ -92,19 +94,81 @@ describe('User Agent module test suite', () => {
       // Then
       expect(result).toBe('Mozilla/5.0 (X11; Fedora; Linux x86_64) AppleWebKit/1337.36 (KHTML, like Gecko) Chrome/1337.1337.1337 Safari/537.36');
     });
-    test('google service, should return Firefox user-agent header', () => {
-      // Given
-      userAgent.BROWSER_VERSIONS.firefox = '133.7';
-      const browserView = {
-        webContents: {
-          userAgent: 'Mozilla/5.0 (X11; Fedora; Linux x86_64) AppleWebKit/1337.36 (KHTML, like Gecko) ElectronIM/13.337.0 Chrome/79.0.1337.79 Electron/0.0.99 Safari/537.36'
+  });
+  describe('addUserAgentInterceptor', () => {
+    let session;
+    beforeEach(() => {
+      session = {
+        webRequest: {
+          onBeforeSendHeaders: jest.fn()
         }
       };
-      const nonMatchingUrl = 'https://hangouts.google.com';
+    });
+    test('userAgentInterceptor = true, should return (interceptor only added once)', () => {
+      // Given
+      session.userAgentInterceptor = true;
       // When
-      const result = userAgent.userAgentForView(browserView, nonMatchingUrl);
+      userAgent.addUserAgentInterceptor(session);
+      // Then
+      expect(session.webRequest.onBeforeSendHeaders).not.toHaveBeenCalled();
+    });
+    test('userAgentInterceptor = undefined, should add interceptor', () => {
+      // When
+      userAgent.addUserAgentInterceptor(session);
+      // Then
+      expect(session.userAgentInterceptor).toEqual(true);
+      expect(session.webRequest.onBeforeSendHeaders).toHaveBeenCalledTimes(1);
+    });
+    test('onBeforeSendHeaders interceptor,filter', () => {
+      // When
+      userAgent.addUserAgentInterceptor(session);
+      // Then
+      expect(session.webRequest.onBeforeSendHeaders.mock.calls[0][0].urls)
+        .toEqual(['*://*.google.com/*']);
+    });
+    test('onBeforeSendHeaders interceptor, with standard URl', () => {
+      // Given
+      const details = {
+        url: 'https://www.example.com',
+        requestHeaders: {}
+      };
+      const callback = jest.fn();
+      userAgent.addUserAgentInterceptor(session);
+      // When
+      session.webRequest.onBeforeSendHeaders.mock.calls[0][1](details, callback);
       // Then
-      expect(result).toBe('Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:133.7) Gecko/20100101 Firefox/133.7');
+      expect(details.requestHeaders).toBeEmpty();
+    });
+    describe('onBeforeSendHeaders', () => {
+      let details;
+      const callback = jest.fn();
+      beforeEach(() => {
+        userAgent.BROWSER_VERSIONS.firefoxESR = '133.7';
+        details = {
+          requestHeaders: {
+            'User-Agent': 'Mozilla/5.0 (X11; Fedora; Linux x86_64) AppleWebKit/1337.36 (KHTML, like Gecko) ElectronIM/13.337.0 Chrome/79.0.1337.79 Electron/0.0.99 Safari/537.36'
+          }
+        };
+        userAgent.addUserAgentInterceptor(session);
+      });
+      test('onBeforeSendHeaders interceptor, with Google URl', () => {
+        // Given
+        details.url = 'https://accounts.google.com';
+        // When
+        session.webRequest.onBeforeSendHeaders.mock.calls[0][1](details, callback);
+        // Then
+        expect(details.requestHeaders['User-Agent'])
+          .toBe('Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:133.7) Gecko/20100101 Firefox/133.7');
+      });
+      test('onBeforeSendHeaders interceptor, with Google meet URl', () => {
+        // Given
+        details.url = 'https://meet.google.com';
+        // When
+        session.webRequest.onBeforeSendHeaders.mock.calls[0][1](details, callback);
+        // Then
+        expect(details.requestHeaders['User-Agent'])
+          .toBe('Mozilla/5.0 (X11; Fedora; Linux x86_64) AppleWebKit/1337.36 (KHTML, like Gecko) ElectronIM/13.337.0 Chrome/79.0.1337.79 Electron/0.0.99 Safari/537.36');
+      });
     });
   });
 });