Browse Source

fix(extensions): implement missing web_request hooks (#27097)

* fix(extensions): implement missing web_request hooks (#22655)

Co-authored-by: Jeremy Apthorp <[email protected]>
Co-authored-by: samuelmaddock <[email protected]>

* fix: remove ukm_source_id

Co-authored-by: Jeremy Apthorp <[email protected]>
Co-authored-by: samuelmaddock <[email protected]>
Eryk Rakowski 4 years ago
parent
commit
700c410ec3

+ 6 - 0
docs/api/extensions.md

@@ -114,3 +114,9 @@ The following methods of `chrome.management` are supported:
 - `chrome.management.getPermissionWarningsByManifest`
 - `chrome.management.onEnabled`
 - `chrome.management.onDisabled`
+
+### `chrome.webRequest`
+
+All features of this API are supported.
+
+> **NOTE:** Electron's [`webRequest`](web-request.md) module takes precedence over `chrome.webRequest` if there are conflicting handlers.

+ 46 - 1
shell/browser/electron_browser_client.cc

@@ -138,6 +138,7 @@
 #include "content/public/browser/file_url_loader.h"
 #include "content/public/browser/web_ui_url_loader_factory.h"
 #include "extensions/browser/api/mime_handler_private/mime_handler_private.h"
+#include "extensions/browser/api/web_request/web_request_api.h"
 #include "extensions/browser/browser_context_keyed_api_factory.h"
 #include "extensions/browser/extension_host.h"
 #include "extensions/browser/extension_message_filter.h"
@@ -1440,7 +1441,17 @@ bool ElectronBrowserClient::WillInterceptWebSocket(
   if (!web_request.get())
     return false;
 
-  return web_request->HasListener();
+  bool has_listener = web_request->HasListener();
+#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
+  const auto* web_request_api =
+      extensions::BrowserContextKeyedAPIFactory<extensions::WebRequestAPI>::Get(
+          browser_context);
+
+  if (web_request_api)
+    has_listener |= web_request_api->MayHaveProxies();
+#endif
+
+  return has_listener;
 }
 
 void ElectronBrowserClient::CreateWebSocket(
@@ -1454,8 +1465,24 @@ void ElectronBrowserClient::CreateWebSocket(
   v8::Isolate* isolate = v8::Isolate::GetCurrent();
   v8::HandleScope scope(isolate);
   auto* browser_context = frame->GetProcess()->GetBrowserContext();
+
   auto web_request = api::WebRequest::FromOrCreate(isolate, browser_context);
   DCHECK(web_request.get());
+
+#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
+  if (!web_request->HasListener()) {
+    auto* web_request_api = extensions::BrowserContextKeyedAPIFactory<
+        extensions::WebRequestAPI>::Get(browser_context);
+
+    if (web_request_api && web_request_api->MayHaveProxies()) {
+      web_request_api->ProxyWebSocket(frame, std::move(factory), url,
+                                      site_for_cookies.RepresentativeUrl(),
+                                      user_agent, std::move(handshake_client));
+      return;
+    }
+  }
+#endif
+
   ProxyingWebSocket::StartProxying(
       web_request.get(), std::move(factory), url,
       site_for_cookies.RepresentativeUrl(), user_agent,
@@ -1482,6 +1509,24 @@ bool ElectronBrowserClient::WillCreateURLLoaderFactory(
   auto web_request = api::WebRequest::FromOrCreate(isolate, browser_context);
   DCHECK(web_request.get());
 
+#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
+  if (!web_request->HasListener()) {
+    auto* web_request_api = extensions::BrowserContextKeyedAPIFactory<
+        extensions::WebRequestAPI>::Get(browser_context);
+
+    DCHECK(web_request_api);
+    bool use_proxy_for_web_request =
+        web_request_api->MaybeProxyURLLoaderFactory(
+            browser_context, frame_host, render_process_id, type, navigation_id,
+            factory_receiver, header_client);
+
+    if (bypass_redirect_checks)
+      *bypass_redirect_checks = use_proxy_for_web_request;
+    if (use_proxy_for_web_request)
+      return true;
+  }
+#endif
+
   auto proxied_receiver = std::move(*factory_receiver);
   mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory_remote;
   *factory_receiver = target_factory_remote.InitWithNewPipeAndPassReceiver();

+ 20 - 0
shell/browser/extensions/electron_extension_loader.cc

@@ -12,12 +12,15 @@
 #include "base/files/file_util.h"
 #include "base/logging.h"
 #include "base/sequenced_task_runner.h"
+#include "base/strings/string_number_conversions.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/task_runner_util.h"
 #include "base/threading/thread_restrictions.h"
+#include "base/time/time.h"
 #include "extensions/browser/extension_file_task_runner.h"
 #include "extensions/browser/extension_prefs.h"
 #include "extensions/browser/extension_registry.h"
+#include "extensions/browser/pref_names.h"
 #include "extensions/common/file_util.h"
 
 namespace extensions {
@@ -110,6 +113,23 @@ void ElectronExtensionLoader::FinishExtensionLoad(
   if (extension) {
     extension_registrar_.AddExtension(extension);
   }
+
+  // Write extension install time to ExtensionPrefs. This is required by
+  // WebRequestAPI which calls extensions::ExtensionPrefs::GetInstallTime.
+  //
+  // Implementation for writing the pref was based on
+  // PreferenceAPIBase::SetExtensionControlledPref.
+  {
+    ExtensionPrefs* extension_prefs = ExtensionPrefs::Get(browser_context_);
+    ExtensionPrefs::ScopedDictionaryUpdate update(
+        extension_prefs, extension.get()->id(),
+        extensions::pref_names::kPrefPreferences);
+    auto preference = update.Create();
+    const base::Time install_time = base::Time().Now();
+    preference->SetString("install_time",
+                          base::NumberToString(install_time.ToInternalValue()));
+  }
+
   std::move(cb).Run(extension.get(), result.second);
 }
 

+ 1 - 3
shell/common/extensions/electron_extensions_client.cc

@@ -130,9 +130,7 @@ const GURL& ElectronExtensionsClient::GetWebstoreUpdateURL() const {
 }
 
 bool ElectronExtensionsClient::IsBlacklistUpdateURL(const GURL& url) const {
-  // TODO(rockot): Maybe we want to do something else here. For now we accept
-  // any URL as a blacklist URL because we don't really care.
-  return true;
+  return false;
 }
 
 }  // namespace electron

+ 102 - 21
spec-main/extensions-spec.ts

@@ -1,12 +1,15 @@
 import { expect } from 'chai';
-import { app, session, BrowserWindow, ipcMain, WebContents, Extension } from 'electron/main';
+import { app, session, BrowserWindow, ipcMain, WebContents, Extension, Session } from 'electron/main';
 import { closeAllWindows, closeWindow } from './window-helpers';
 import * as http from 'http';
 import { AddressInfo } from 'net';
 import * as path from 'path';
 import * as fs from 'fs';
+import * as WebSocket from 'ws';
 import { emittedOnce, emittedNTimes } from './events-helpers';
 
+const uuid = require('uuid');
+
 const fixtures = path.join(__dirname, 'fixtures');
 
 describe('chrome extensions', () => {
@@ -15,6 +18,7 @@ describe('chrome extensions', () => {
   // NB. extensions are only allowed on http://, https:// and ftp:// (!) urls by default.
   let server: http.Server;
   let url: string;
+  let port: string;
   before(async () => {
     server = http.createServer((req, res) => {
       if (req.url === '/cors') {
@@ -22,8 +26,19 @@ describe('chrome extensions', () => {
       }
       res.end(emptyPage);
     });
+
+    const wss = new WebSocket.Server({ noServer: true });
+    wss.on('connection', function connection (ws) {
+      ws.on('message', function incoming (message) {
+        if (message === 'foo') {
+          ws.send('bar');
+        }
+      });
+    });
+
     await new Promise(resolve => server.listen(0, '127.0.0.1', () => {
-      url = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
+      port = String((server.address() as AddressInfo).port);
+      url = `http://127.0.0.1:${port}`;
       resolve();
     }));
   });
@@ -87,7 +102,7 @@ describe('chrome extensions', () => {
     // extension registry is redirected to the main session. so installing an
     // extension in an in-memory session results in it being installed in the
     // default session.
-    const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
+    const customSession = session.fromPartition(`persist:${uuid.v4()}`);
     await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
     const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
     w.loadURL(url);
@@ -99,7 +114,7 @@ describe('chrome extensions', () => {
   it('serializes a loaded extension', async () => {
     const extensionPath = path.join(fixtures, 'extensions', 'red-bg');
     const manifest = JSON.parse(fs.readFileSync(path.join(extensionPath, 'manifest.json'), 'utf-8'));
-    const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
+    const customSession = session.fromPartition(`persist:${uuid.v4()}`);
     const extension = await customSession.loadExtension(extensionPath);
     expect(extension.id).to.be.a('string');
     expect(extension.name).to.be.a('string');
@@ -110,7 +125,7 @@ describe('chrome extensions', () => {
   });
 
   it('removes an extension', async () => {
-    const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
+    const customSession = session.fromPartition(`persist:${uuid.v4()}`);
     const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
     {
       const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
@@ -128,7 +143,7 @@ describe('chrome extensions', () => {
   });
 
   it('lists loaded extensions in getAllExtensions', async () => {
-    const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
+    const customSession = session.fromPartition(`persist:${uuid.v4()}`);
     const e = await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
     expect(customSession.getAllExtensions()).to.deep.equal([e]);
     customSession.removeExtension(e.id);
@@ -136,13 +151,13 @@ describe('chrome extensions', () => {
   });
 
   it('gets an extension by id', async () => {
-    const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
+    const customSession = session.fromPartition(`persist:${uuid.v4()}`);
     const e = await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
     expect(customSession.getExtension(e.id)).to.deep.equal(e);
   });
 
   it('confines an extension to the session it was loaded in', async () => {
-    const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
+    const customSession = session.fromPartition(`persist:${uuid.v4()}`);
     await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
     const w = new BrowserWindow({ show: false }); // not in the session
     w.loadURL(url);
@@ -152,7 +167,7 @@ describe('chrome extensions', () => {
   });
 
   it('loading an extension in a temporary session throws an error', async () => {
-    const customSession = session.fromPartition(require('uuid').v4());
+    const customSession = session.fromPartition(uuid.v4());
     await expect(customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'))).to.eventually.be.rejectedWith('Extensions cannot be loaded in a temporary session');
   });
 
@@ -166,7 +181,7 @@ describe('chrome extensions', () => {
       return result;
     };
     beforeEach(async () => {
-      const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
+      const customSession = session.fromPartition(`persist:${uuid.v4()}`);
       extension = await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-i18n'));
       w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });
       w.loadURL(url);
@@ -186,7 +201,7 @@ describe('chrome extensions', () => {
   describe('chrome.runtime', () => {
     let content: any;
     before(async () => {
-      const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
+      const customSession = session.fromPartition(`persist:${uuid.v4()}`);
       await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-runtime'));
       const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
       try {
@@ -211,7 +226,7 @@ describe('chrome extensions', () => {
 
   describe('chrome.storage', () => {
     it('stores and retrieves a key', async () => {
-      const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
+      const customSession = session.fromPartition(`persist:${uuid.v4()}`);
       await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-storage'));
       const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });
       try {
@@ -225,9 +240,75 @@ describe('chrome extensions', () => {
     });
   });
 
+  describe('chrome.webRequest', () => {
+    function fetch (contents: WebContents, url: string) {
+      return contents.executeJavaScript(`fetch(${JSON.stringify(url)})`);
+    }
+
+    let customSession: Session;
+    let w: BrowserWindow;
+
+    beforeEach(() => {
+      customSession = session.fromPartition(`persist:${uuid.v4()}`);
+      w = new BrowserWindow({ show: false, webPreferences: { session: customSession, sandbox: true, contextIsolation: true } });
+    });
+
+    describe('onBeforeRequest', () => {
+      it('can cancel http requests', async () => {
+        await w.loadURL(url);
+        await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest'));
+        await expect(fetch(w.webContents, url)).to.eventually.be.rejectedWith(TypeError);
+      });
+
+      it('does not cancel http requests when no extension loaded', async () => {
+        await w.loadURL(url);
+        await expect(fetch(w.webContents, url)).to.not.be.rejectedWith(TypeError);
+      });
+    });
+
+    it('does not take precedence over Electron webRequest - http', async () => {
+      return new Promise((resolve) => {
+        (async () => {
+          customSession.webRequest.onBeforeRequest((details, callback) => {
+            resolve();
+            callback({ cancel: true });
+          });
+          await w.loadURL(url);
+
+          await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest'));
+          fetch(w.webContents, url);
+        })();
+      });
+    });
+
+    it('does not take precedence over Electron webRequest - WebSocket', () => {
+      return new Promise((resolve) => {
+        (async () => {
+          customSession.webRequest.onBeforeSendHeaders(() => {
+            resolve();
+          });
+          await w.loadFile(path.join(fixtures, 'api', 'webrequest.html'), { query: { port } });
+          await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest-wss'));
+        })();
+      });
+    });
+
+    describe('WebSocket', () => {
+      it('can be proxied', async () => {
+        await w.loadFile(path.join(fixtures, 'api', 'webrequest.html'), { query: { port } });
+        await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest-wss'));
+        customSession.webRequest.onSendHeaders((details) => {
+          if (details.url.startsWith('ws://')) {
+            expect(details.requestHeaders.foo).be.equal('bar');
+          }
+        });
+      });
+    });
+  });
+
   describe('chrome.tabs', () => {
     it('executeScript', async () => {
-      const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
+      const customSession = session.fromPartition(`persist:${uuid.v4()}`);
       await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-api'));
       const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });
       await w.loadURL(url);
@@ -242,12 +323,12 @@ describe('chrome extensions', () => {
     });
 
     it('connect', async () => {
-      const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
+      const customSession = session.fromPartition(`persist:${uuid.v4()}`);
       await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-api'));
       const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });
       await w.loadURL(url);
 
-      const portName = require('uuid').v4();
+      const portName = uuid.v4();
       const message = { method: 'connectTab', args: [portName] };
       w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
 
@@ -258,7 +339,7 @@ describe('chrome extensions', () => {
     });
 
     it('sendMessage receives the response', async function () {
-      const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
+      const customSession = session.fromPartition(`persist:${uuid.v4()}`);
       await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-api'));
       const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });
       await w.loadURL(url);
@@ -276,7 +357,7 @@ describe('chrome extensions', () => {
 
   describe('background pages', () => {
     it('loads a lazy background page when sending a message', async () => {
-      const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
+      const customSession = session.fromPartition(`persist:${uuid.v4()}`);
       await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page'));
       const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });
       try {
@@ -292,7 +373,7 @@ describe('chrome extensions', () => {
     });
 
     it('can use extension.getBackgroundPage from a ui page', async () => {
-      const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
+      const customSession = session.fromPartition(`persist:${uuid.v4()}`);
       const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page'));
       const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
       await w.loadURL(`chrome-extension://${id}/page-get-background.html`);
@@ -301,7 +382,7 @@ describe('chrome extensions', () => {
     });
 
     it('can use extension.getBackgroundPage from a ui page', async () => {
-      const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
+      const customSession = session.fromPartition(`persist:${uuid.v4()}`);
       const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page'));
       const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
       await w.loadURL(`chrome-extension://${id}/page-get-background.html`);
@@ -310,7 +391,7 @@ describe('chrome extensions', () => {
     });
 
     it('can use runtime.getBackgroundPage from a ui page', async () => {
-      const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
+      const customSession = session.fromPartition(`persist:${uuid.v4()}`);
       const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page'));
       const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
       await w.loadURL(`chrome-extension://${id}/page-runtime-get-background.html`);
@@ -370,7 +451,7 @@ describe('chrome extensions', () => {
     };
 
     it('loads a devtools extension', async () => {
-      const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
+      const customSession = session.fromPartition(`persist:${uuid.v4()}`);
       customSession.loadExtension(path.join(fixtures, 'extensions', 'devtools-extension'));
       const winningMessage = emittedOnce(ipcMain, 'winning');
       const w = new BrowserWindow({ show: true, webPreferences: { session: customSession, nodeIntegration: true } });

+ 12 - 0
spec-main/fixtures/extensions/chrome-webRequest-wss/background.js

@@ -0,0 +1,12 @@
+/* global chrome */
+
+chrome.webRequest.onBeforeSendHeaders.addListener(
+  (details) => {
+    if (details.requestHeaders) {
+      details.requestHeaders.foo = 'bar';
+    }
+    return { cancel: false, requestHeaders: details.requestHeaders };
+  },
+  { urls: ['*://127.0.0.1:*'] },
+  ['blocking']
+);

+ 10 - 0
spec-main/fixtures/extensions/chrome-webRequest-wss/manifest.json

@@ -0,0 +1,10 @@
+{
+  "name": "chrome-webRequest",
+  "version": "1.0",
+  "background": {
+    "scripts": ["background.js"],
+    "persistent": true
+  },
+  "permissions": ["webRequest", "webRequestBlocking", "<all_urls>"],
+  "manifest_version": 2
+}

+ 9 - 0
spec-main/fixtures/extensions/chrome-webRequest/background.js

@@ -0,0 +1,9 @@
+/* global chrome */
+
+chrome.webRequest.onBeforeRequest.addListener(
+  (details) => {
+    return { cancel: true };
+  },
+  { urls: ['*://127.0.0.1:*'] },
+  ['blocking']
+);

+ 10 - 0
spec-main/fixtures/extensions/chrome-webRequest/manifest.json

@@ -0,0 +1,10 @@
+{
+  "name": "chrome-webRequest",
+  "version": "1.0",
+  "background": {
+    "scripts": ["background.js"],
+    "persistent": true
+  },
+  "permissions": ["webRequest", "webRequestBlocking", "<all_urls>"],
+  "manifest_version": 2
+}