зеркало из https://github.com/electron/electron.git
fix(extensions): implement missing web_request hooks (#22655)
Co-authored-by: Jeremy Apthorp <nornagon@nornagon.net> Co-authored-by: samuelmaddock <samuel.maddock@gmail.com>
This commit is contained in:
Родитель
8f1bc338e5
Коммит
bf24759354
|
@ -115,3 +115,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.
|
||||
|
|
|
@ -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"
|
||||
|
@ -1436,7 +1437,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(
|
||||
|
@ -1450,8 +1461,24 @@ void ElectronBrowserClient::CreateWebSocket(
|
|||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
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,
|
||||
|
@ -1479,6 +1506,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,
|
||||
ukm_source_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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}));
|
||||
});
|
||||
|
@ -84,7 +99,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 } });
|
||||
await w.loadURL(url);
|
||||
|
@ -95,7 +110,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');
|
||||
|
@ -106,7 +121,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 } });
|
||||
|
@ -141,7 +156,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);
|
||||
|
@ -149,13 +164,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
|
||||
await w.loadURL(url);
|
||||
|
@ -164,7 +179,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');
|
||||
});
|
||||
|
||||
|
@ -178,7 +193,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 } });
|
||||
await w.loadURL(url);
|
||||
|
@ -203,7 +218,7 @@ describe('chrome extensions', () => {
|
|||
return result;
|
||||
};
|
||||
beforeEach(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'));
|
||||
w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });
|
||||
await w.loadURL(url);
|
||||
|
@ -231,7 +246,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 {
|
||||
|
@ -245,9 +260,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);
|
||||
|
@ -262,12 +343,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)}', '*')`);
|
||||
|
||||
|
@ -278,7 +359,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);
|
||||
|
@ -296,7 +377,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 {
|
||||
|
@ -312,7 +393,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`);
|
||||
|
@ -321,7 +402,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`);
|
||||
|
@ -330,7 +411,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`);
|
||||
|
@ -390,7 +471,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 } });
|
||||
|
|
|
@ -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']
|
||||
);
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "chrome-webRequest",
|
||||
"version": "1.0",
|
||||
"background": {
|
||||
"scripts": ["background.js"],
|
||||
"persistent": true
|
||||
},
|
||||
"permissions": ["webRequest", "webRequestBlocking", "<all_urls>"],
|
||||
"manifest_version": 2
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/* global chrome */
|
||||
|
||||
chrome.webRequest.onBeforeRequest.addListener(
|
||||
(details) => {
|
||||
return { cancel: true };
|
||||
},
|
||||
{ urls: ['*://127.0.0.1:*'] },
|
||||
['blocking']
|
||||
);
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "chrome-webRequest",
|
||||
"version": "1.0",
|
||||
"background": {
|
||||
"scripts": ["background.js"],
|
||||
"persistent": true
|
||||
},
|
||||
"permissions": ["webRequest", "webRequestBlocking", "<all_urls>"],
|
||||
"manifest_version": 2
|
||||
}
|
Загрузка…
Ссылка в новой задаче