Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion manifest.chromium.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,24 @@
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; img-src 'self' data: blob:; style-src 'self' 'nonce-cloudhood-extension-style-nonce';"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["mock-requests.js"],
"run_at": "document_start",
"world": "MAIN"
},
{
"matches": ["<all_urls>"],
"js": ["content-script.js"],
"run_at": "document_start"
}
],
"web_accessible_resources": [
{
"resources": [
"img/*"
"img/*",
"mock-requests.js"
],
"matches": [
"<all_urls>"
Expand Down
15 changes: 14 additions & 1 deletion manifest.dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,22 @@
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; connect-src 'self' ws://localhost:3333; img-src 'self' data: blob:; style-src 'self' 'nonce-cloudhood-extension-style-nonce';"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["mock-requests.js"],
"run_at": "document_start",
"world": "MAIN"
},
{
"matches": ["<all_urls>"],
"js": ["content-script.js"],
"run_at": "document_start"
}
],
"web_accessible_resources": [
{
"resources": ["img/*"],
"resources": ["img/*", "mock-requests.js"],
"matches": ["<all_urls>"]
}
]
Expand Down
16 changes: 15 additions & 1 deletion manifest.firefox.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,24 @@
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; img-src 'self' data: blob: moz-extension:; style-src 'self' 'nonce-cloudhood-extension-style-nonce'; connect-src 'self' data: blob: moz-extension:;"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["mock-requests.js"],
"run_at": "document_start",
"world": "MAIN"
},
{
"matches": ["<all_urls>"],
"js": ["content-script.js"],
"run_at": "document_start"
}
],
"web_accessible_resources": [
{
"resources": [
"img/*"
"img/*",
"mock-requests.js"
],
"matches": [
"<all_urls>"
Expand Down
68 changes: 68 additions & 0 deletions src/assets/content-script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// This script runs in the isolated extension world
// It reads storage and communicates with the MAIN world script via window.postMessage

(function() {
// Helper to communicate with the injected script
function sendOverrides(overrides) {
window.postMessage(
{
type: 'CLOUDHOOD_UPDATE_OVERRIDES',
overrides,
},
window.location.origin,
);
}

// Read storage and update overrides
function updateOverrides() {
chrome.storage.local.get(
['requestHeaderProfilesV1', 'selectedHeaderProfileV1', 'isPausedV1'],
(result) => {
const isPaused = result.isPausedV1;
if (isPaused) {
sendOverrides([]);
return;
}

let profiles = [];
const profilesData = result.requestHeaderProfilesV1;

try {
if (typeof profilesData === 'string') {
profiles = JSON.parse(profilesData);
} else if (Array.isArray(profilesData)) {
profiles = profilesData;
}
} catch {
// ignore
}

const selectedProfileId = result.selectedHeaderProfileV1;
const profile = profiles.find(p => p.id === selectedProfileId);

if (profile && profile.responseOverrides) {
const activeOverrides = profile.responseOverrides
.filter(o => !o.disabled && o.urlPattern && o.responseContent)
.map(o => ({
urlPattern: o.urlPattern,
responseContent: o.responseContent,
}));

sendOverrides(activeOverrides);
} else {
sendOverrides([]);
}
}
);
}

// Listen for storage changes
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName === 'local') {
updateOverrides();
}
});

// Initial update
updateOverrides();
})();
137 changes: 137 additions & 0 deletions src/assets/mock-requests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/* eslint-disable no-console */
(function () {
// Configuration
let activeOverrides = [];

// Store original functions
const originalFetch = window.fetch;
const OriginalXHR = window.XMLHttpRequest;

const log = (title, data) => {
console.groupCollapsed(`%c[Cloudhood] ${title}`, 'color: #00aa00; font-weight: bold;');
console.log('Data:', data);
console.trace('Stack Trace');
console.groupEnd();
}


// Listen for updates from the extension
window.addEventListener('message', (event) => {
if (event.source !== window) return;

if (event.data?.type === 'CLOUDHOOD_UPDATE_OVERRIDES') {
activeOverrides = event.data.overrides.map((o) => ({
urlPattern: o.urlPattern,
responseContent: o.responseContent,
}));
}
});

// Patch Fetch
window.fetch = async (input, init) => {
let url;

if (typeof input === 'string') {
url = input;
} else if (input instanceof URL) {
url = input.toString();
} else if (input instanceof Request) {
url = input.url;
} else {
url = String(input);
}

const override = activeOverrides.find(o => url.includes(o.urlPattern));

if (override) {
try {
const parsedResponse = JSON.parse(override.responseContent);

log(`Mocked Fetch: ${url}`, {
url,
response: parsedResponse,
originalInput: input,
originalInit: init
});

return new Response(override.responseContent, {
status: 200,
statusText: 'OK',
headers: {
'content-type': 'application/json',
}
});
} catch (error) {
console.warn('[Cloudhood] Invalid JSON in override for URL:', url, error);
return originalFetch(input, init);
}
}

return originalFetch(input, init);
};

// Patch XHR
window.XMLHttpRequest = class CloudhoodXHR extends OriginalXHR {
open(method, url, async = true, username = null, password = null) {
this._method = method;
this._url = url.toString();

this._override = activeOverrides.find(o => this._url.includes(o.urlPattern));

super.open(method, url, async, username, password);
}

send(body) {
if (this._override) {
try {
const parsedResponse = JSON.parse(this._override.responseContent);

log(`Mocked XHR: ${this._url}`, {
url: this._url,
method: this._method,
response: parsedResponse,
body
});

setTimeout(() => {
const responseData = this._override.responseContent;

Object.defineProperty(this, 'readyState', { value: 4, writable: false });
Object.defineProperty(this, 'status', { value: 200, writable: false });
Object.defineProperty(this, 'statusText', { value: 'OK', writable: false });
Object.defineProperty(this, 'responseText', { value: responseData, writable: false });
Object.defineProperty(this, 'response', { value: responseData, writable: false });
Object.defineProperty(this, 'responseURL', { value: this._url, writable: false });

this.dispatchEvent(new Event('readystatechange'));

const progressEvent = new ProgressEvent('load', {
loaded: responseData.length,
total: responseData.length,
lengthComputable: true
});

this.dispatchEvent(progressEvent);
this.dispatchEvent(new ProgressEvent('loadend'));

if (this.onreadystatechange) {
this.onreadystatechange(new Event('readystatechange'));
}
if (this.onload) {
this.onload(progressEvent);
}
if (this.onloadend) {
this.onloadend(new ProgressEvent('loadend'));
}
}, 10);

return;
} catch (error) {
console.warn('[Cloudhood] Invalid JSON in override for XHR URL:', this._url, error);
}
}

super.send(body);
}
};
})();
18 changes: 12 additions & 6 deletions src/background.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import browser from 'webextension-polyfill';

import type { Profile, RequestHeader } from '#entities/request-profile/types';
import type { Profile, RequestHeader, ResponseOverride } from '#entities/request-profile/types';

import { BrowserStorageKey, ServiceWorkerEvent } from './shared/constants';
import { browserAction } from './shared/utils/browserAPI';
Expand Down Expand Up @@ -36,19 +36,25 @@ logger.info('🔍 About to check storage contents...');
logger.info(' - Is Paused:', result[BrowserStorageKey.IsPaused] || false);

// Логируем количество профилей, если они есть
let activeHeadersCount = 0;
let activeRulesCount = 0;
if (result[BrowserStorageKey.Profiles]) {
try {
const profiles = JSON.parse(result[BrowserStorageKey.Profiles] as string);
logger.info(` - Profiles count: ${profiles.length}`);
if (profiles.length > 0) {
logger.info(' - Profile names:', profiles.map((p: Profile) => p.name || p.id).join(', '));

// Подсчитываем активные заголовки для badge
// Подсчитываем активные правила для badge
const selectedProfile = profiles.find((p: Profile) => p.id === result[BrowserStorageKey.SelectedProfile]);
if (selectedProfile) {
activeHeadersCount = selectedProfile.requestHeaders?.filter((h: RequestHeader) => !h.disabled).length || 0;
const activeHeadersCount =
selectedProfile.requestHeaders?.filter((h: RequestHeader) => !h.disabled).length || 0;
const activeOverridesCount =
selectedProfile.responseOverrides?.filter((o: ResponseOverride) => !o.disabled).length || 0;
activeRulesCount = activeHeadersCount + activeOverridesCount;
logger.info(` - Active headers count: ${activeHeadersCount}`);
logger.info(` - Active overrides count: ${activeOverridesCount}`);
logger.info(` - Total active rules count: ${activeRulesCount}`);
}
}
} catch (error) {
Expand All @@ -61,8 +67,8 @@ logger.info('🔍 About to check storage contents...');

// Устанавливаем badge на основе данных из storage
const isPaused = (result[BrowserStorageKey.IsPaused] as boolean) || false;
await setIconBadge({ isPaused, activeRulesCount: activeHeadersCount });
logger.info(`🏷️ Badge set: paused=${isPaused}, activeRules=${activeHeadersCount}`);
await setIconBadge({ isPaused, activeRulesCount });
logger.info(`🏷️ Badge set: paused=${isPaused}, activeRules=${activeRulesCount}`);
} catch (error) {
logger.error('Failed to check storage on background script load:', error);
}
Expand Down
2 changes: 1 addition & 1 deletion src/entities/profile-actions/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createEvent, createStore } from 'effector';

import { selectedRequestProfileIdChanged } from '#entities/request-profile/model/selected-request-profile';

export type ProfileActionsTab = 'headers' | 'url-filters';
export type ProfileActionsTab = 'headers' | 'url-filters' | 'overrides';

export const profileActionsTabChanged = createEvent<ProfileActionsTab>();

Expand Down
1 change: 1 addition & 0 deletions src/entities/request-profile/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './request-profiles';
export * from './selected-profile-url-filters';
export * from './selected-request-headers';
export * from './selected-request-profile';
export * from './selected-overrides';
1 change: 1 addition & 0 deletions src/entities/request-profile/model/request-profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const profileAddedFx = attach({
id: addedHeaderId,
requestHeaders: [{ id: generateId(), name: '', value: '', disabled: false }],
urlFilters: [{ id: generateId(), value: '', disabled: false }],
responseOverrides: [{ id: generateId(), urlPattern: '', responseContent: '', disabled: false }],
},
],
addedHeaderId,
Expand Down
17 changes: 17 additions & 0 deletions src/entities/request-profile/model/selected-overrides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { combine } from 'effector';

import { $requestProfiles } from './request-profiles';
import { $selectedRequestProfile } from './selected-request-profile';

export const $selectedProfileResponseOverrides = combine(
$selectedRequestProfile,
$requestProfiles,
(selectedProfileId, profiles) => profiles.find(p => p.id === selectedProfileId)?.responseOverrides ?? [],
{ skipVoid: false },
);

export const $selectedProfileActiveResponseOverridesCount = combine(
$selectedProfileResponseOverrides,
overrides => overrides.filter(item => !item.disabled && item.urlPattern.trim() && item.responseContent.trim()).length,
{ skipVoid: false },
);
15 changes: 14 additions & 1 deletion src/entities/request-profile/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,20 @@ export type UrlFilter = {
disabled: boolean;
};

export type Profile = { id: string; name?: string; requestHeaders: RequestHeader[]; urlFilters: UrlFilter[] };
export type ResponseOverride = {
id: number;
urlPattern: string;
responseContent: string;
disabled: boolean;
};

export type Profile = {
id: string;
name?: string;
requestHeaders: RequestHeader[];
urlFilters: UrlFilter[];
responseOverrides?: ResponseOverride[];
};

export type RemoveHeaderPayload = {
headerId: number;
Expand Down
Loading