Реалізація перехоплення та модифікації HTTP-запитів у розширенні браузера
Перехоплення HTTP-запитів — одна з найпотужніших можливостей розширень. Блокувальники реклами, VPN-розширення, прокси-інструменти, інжектори заголовків — усе це будується на chrome.webRequest (MV2) або chrome.declarativeNetRequest (MV3). Перехід на MV3 кардинально змінив підхід.
MV3: declarativeNetRequest
У Manifest V3 динамічна модифікація запитів замінена на декларативні правила. Браузер застосовує правила сам, без виконання JS-коду розширення. Це швидше та безпечніше, але менш гнучко.
{
"manifest_version": 3,
"permissions": ["declarativeNetRequest", "declarativeNetRequestWithHostAccess"],
"host_permissions": ["<all_urls>"],
"declarative_net_request": {
"rule_resources": [
{
"id": "static-rules",
"enabled": true,
"path": "rules/static.json"
}
]
}
}
Статичні правила
Файл rules/static.json містить масив правил, які застосовуються постійно:
[
{
"id": 1,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "||analytics.example.com^",
"resourceTypes": ["script", "xmlhttprequest", "image"]
}
},
{
"id": 2,
"priority": 2,
"action": {
"type": "modifyHeaders",
"requestHeaders": [
{ "header": "X-Custom-Token", "operation": "set", "value": "my-token" },
{ "header": "Referer", "operation": "remove" }
]
},
"condition": {
"urlFilter": "https://api.internal.corp/*",
"resourceTypes": ["xmlhttprequest"]
}
},
{
"id": 3,
"priority": 1,
"action": {
"type": "redirect",
"redirect": { "regexSubstitution": "https://cdn.example.com\\1" }
},
"condition": {
"regexFilter": "^https://slow-cdn\\.com(.*)",
"resourceTypes": ["image", "media", "font"]
}
}
]
Ліміти: максимум 30000 статичних правил сумарно по всім файлам, 5000 включених правил одночасно (можна змінювати динамічно).
Динамічні правила з service worker
Правила можна додавати та видаляти в рантаймі:
// background/sw.js
async function blockDomain(domain) {
const existingRules = await chrome.declarativeNetRequest.getDynamicRules();
const maxId = existingRules.reduce((max, r) => Math.max(max, r.id), 0);
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [{
id: maxId + 1,
priority: 10,
action: { type: 'block' },
condition: {
urlFilter: `||${domain}^`,
resourceTypes: [
'main_frame', 'sub_frame', 'script', 'stylesheet',
'image', 'xmlhttprequest', 'other'
]
}
}],
removeRuleIds: []
});
}
async function unblockDomain(domain) {
const rules = await chrome.declarativeNetRequest.getDynamicRules();
const toRemove = rules
.filter(r => r.condition.urlFilter === `||${domain}^`)
.map(r => r.id);
if (toRemove.length > 0) {
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [],
removeRuleIds: toRemove
});
}
}
// Модифікація заголовків з динамічним значенням
async function setAuthHeader(apiUrl, token) {
const rules = await chrome.declarativeNetRequest.getDynamicRules();
const existingRule = rules.find(r =>
r.condition.urlFilter === apiUrl && r.action.type === 'modifyHeaders'
);
const newRule = {
id: existingRule?.id ?? (rules.reduce((m, r) => Math.max(m, r.id), 0) + 1),
priority: 5,
action: {
type: 'modifyHeaders',
requestHeaders: [
{ header: 'Authorization', operation: 'set', value: `Bearer ${token}` }
]
},
condition: {
urlFilter: apiUrl,
resourceTypes: ['xmlhttprequest', 'fetch']
}
};
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [newRule],
removeRuleIds: existingRule ? [existingRule.id] : []
});
}
Спостереження за запитами: chrome.webRequest (лише MV2)
У MV2 можна було перехоплювати запити синхронно та модифікувати їх у JS. У MV3 chrome.webRequest доступний лише в режимі спостереження (без блокування):
// MV2 або MV3 (тільки читання, без blocking)
chrome.webRequest.onCompleted.addListener(
(details) => {
if (details.statusCode >= 400) {
logFailedRequest({
url: details.url,
status: details.statusCode,
tabId: details.tabId,
timestamp: details.timeStamp
});
}
},
{ urls: ['https://api.your-service.com/*'] },
['responseHeaders']
);
chrome.webRequest.onBeforeRequest.addListener(
(details) => {
// У MV3 — лише спостереження, без blocking
analytics.track('request', {
url: new URL(details.url).pathname,
method: details.method,
tabId: details.tabId
});
},
{ urls: ['<all_urls>'], types: ['xmlhttprequest'] }
);
Перенаправлення запитів через declarativeNetRequest
Складні редиректи з підстановкою:
// Перенаправити запити до staging на production
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [{
id: 100,
priority: 1,
action: {
type: 'redirect',
redirect: {
regexSubstitution: 'https://api.production.com\\1'
}
},
condition: {
regexFilter: '^https://api\\.staging\\.com(.*)',
resourceTypes: ['xmlhttprequest', 'fetch']
}
}],
removeRuleIds: []
});
Інъекція заголовків відповіді
З дозволом declarativeNetRequestWithHostAccess можна змінювати заголовки відповіді — наприклад, убирати CORS-обмеження для розробки:
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [{
id: 200,
priority: 1,
action: {
type: 'modifyHeaders',
responseHeaders: [
{ header: 'Access-Control-Allow-Origin', operation: 'set', value: '*' },
{ header: 'Access-Control-Allow-Methods', operation: 'set', value: 'GET, POST, PUT, DELETE' },
{ header: 'X-Frame-Options', operation: 'remove' }
]
},
condition: {
urlFilter: 'https://api.internal.corp/*',
resourceTypes: ['xmlhttprequest']
}
}],
removeRuleIds: []
});
Відлагодження правил
// Перевірити, які правила застосувалися до останніх запитів
const matched = await chrome.declarativeNetRequest.getMatchedRules({
tabId: tab.id,
minTimeStamp: Date.now() - 60000
});
console.log('Застосовані правила:', matched.rulesMatchedInfo);
// Перевірити, сработає ли правило для конкретного URL
const result = await chrome.declarativeNetRequest.testMatchOutcome({
url: 'https://analytics.example.com/track',
type: 'xmlhttprequest',
method: 'GET',
tabId: -1,
initiator: 'https://example.com'
});
console.log('Результат:', result.matchedRule); // null або об'єкт правила
Обмеження MV3 та обхідні шляхи
Не можна модифікувати тіло запиту через declarativeNetRequest. Для цього потрібен content script, який перехоплює fetch/XMLHttpRequest через monkey-patching у world: 'MAIN':
// content script з world: 'MAIN'
const originalFetch = window.fetch;
window.fetch = async function(input, init = {}) {
const url = typeof input === 'string' ? input : input.url;
if (url.includes('api.target.com')) {
init.headers = {
...init.headers,
'X-Injected-Header': 'value',
};
// Модифікуємо тіло
if (init.body) {
const body = JSON.parse(init.body);
body.extraField = 'injected';
init.body = JSON.stringify(body);
}
}
return originalFetch.call(this, input, init);
};
Це працює, але має обмеження: monkey-patching не торкається запитів із worker-потоків сторінки і не перехоплює WebSocket.
Для розширень, яким потрібна повна влада над трафіком (корпоративні прокси, інструменти безпеки), MV2 ще доступний за корпоративною політикою Chrome, але не буде підтримуватися у публічному Chrome Web Store.







