Skip to content
Merged
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
3 changes: 2 additions & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"proxy",
"webRequest",
"webRequestAuthProvider",
"activeTab"
"activeTab",
"alarms"
]
}
51 changes: 51 additions & 0 deletions public/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,54 @@
"config_proxy_type_pac": {
"message": "PAC Script"
},
"config_section_pac_url": {
"message": "PAC Script URL"
},
"config_section_pac_url_placeholder": {
"message": "https://example.com/proxy.pac"
},
"config_section_pac_url_hint": {
"message": "Optional. If set, the script body is refreshed periodically in the background."
},
"config_action_pac_fetch": {
"message": "Fetch Now"
},
"config_feedback_pac_fetched": {
"message": "PAC script fetched successfully."
},
"config_feedback_pac_fetch_failed": {
"message": "Failed to fetch PAC script: $1"
},
"config_pac_last_fetched": {
"message": "Last fetched: $1"
},
"config_pac_last_error": {
"message": "Last fetch error: $1"
},
"config_pac_manual_edit_warning": {
"message": "A source URL is set. Manual edits will be overwritten on the next refresh."
},
"config_section_pac_refresh_interval": {
"message": "Refresh Interval"
},
"config_section_pac_refresh_interval_hint": {
"message": "How often the PAC script is re-fetched in the background."
},
"config_pac_refresh_interval_minutes": {
"message": "Every $1 minutes"
},
"config_pac_refresh_interval_one_hour": {
"message": "Every hour"
},
"config_pac_refresh_interval_hours": {
"message": "Every $1 hours"
},
"config_pac_refresh_interval_one_day": {
"message": "Every day"
},
"config_pac_refresh_interval_disabled": {
"message": "Disabled (manual only)"
},
"config_proxy_type_auto": {
"message": "Auto Switch"
},
Expand Down Expand Up @@ -224,6 +272,9 @@
"form_is_required": {
"message": "$1 is required"
},
"form_invalid_url": {
"message": "$1 must be a valid http:// or https:// URL"
},

"feedback_error": {
"message": "Something wrong"
Expand Down
15 changes: 15 additions & 0 deletions src/adapters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ export abstract class BaseAdapter {
return ret;
}

// Fires when local storage changes in any extension context, including
// this one — the browser dispatches the event everywhere.
abstract onLocalStorageChanged(
callback: (changes: Record<string, { newValue?: unknown }>) => void
): void;

// proxy
abstract setProxy(cfg: ProxyConfig): Promise<void>;
abstract clearProxy(): Promise<void>;
Expand Down Expand Up @@ -123,6 +129,15 @@ export abstract class BaseAdapter {
): void;
abstract sendMessage(message: any): Promise<any>;

// alarms
abstract createPeriodicAlarm(
name: string,
periodInMinutes: number
): Promise<void>;
abstract clearAlarm(name: string): Promise<void>;
abstract getAllAlarmNames(): Promise<string[]>;
abstract onAlarm(callback: (name: string) => void): void;

// i18n
abstract currentLocale(): string;
abstract getMessage(key: string, substitutions?: string | string[]): string;
Expand Down
24 changes: 24 additions & 0 deletions src/adapters/chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ export class Chrome extends BaseAdapter {
return ret[key] as T | undefined;
}

onLocalStorageChanged(
callback: (changes: Record<string, { newValue?: unknown }>) => void
): void {
chrome.storage.local.onChanged.addListener(callback);
}

async setProxy(cfg: ProxyConfig): Promise<void> {
await chrome.proxy.settings.set({
value: cfg,
Expand Down Expand Up @@ -123,6 +129,24 @@ export class Chrome extends BaseAdapter {
urls: ["<all_urls>"],
});
}

async createPeriodicAlarm(
name: string,
periodInMinutes: number
): Promise<void> {
await chrome.alarms.create(name, { periodInMinutes });
}
async clearAlarm(name: string): Promise<void> {
await chrome.alarms.clear(name);
}
async getAllAlarmNames(): Promise<string[]> {
const all = await chrome.alarms.getAll();
return all.map((a) => a.name);
}
onAlarm(callback: (name: string) => void): void {
chrome.alarms.onAlarm.addListener((alarm) => callback(alarm.name));
}

currentLocale(): string {
return chrome.i18n.getUILanguage();
}
Expand Down
24 changes: 24 additions & 0 deletions src/adapters/firefox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export class Firefox extends BaseAdapter {
return ret[key];
}

onLocalStorageChanged(
callback: (changes: Record<string, { newValue?: unknown }>) => void
): void {
browser.storage.local.onChanged.addListener(callback);
}

async setProxy(cfg: ProxyConfig): Promise<void> {
const proxyCfg: browser.proxy.ProxyConfig = {};

Expand Down Expand Up @@ -142,6 +148,24 @@ export class Firefox extends BaseAdapter {
urls: ["<all_urls>"],
});
}

async createPeriodicAlarm(
name: string,
periodInMinutes: number
): Promise<void> {
await browser.alarms.create(name, { periodInMinutes });
}
async clearAlarm(name: string): Promise<void> {
await browser.alarms.clear(name);
}
async getAllAlarmNames(): Promise<string[]> {
const all = await browser.alarms.getAll();
return all.map((a) => a.name);
}
onAlarm(callback: (name: string) => void): void {
browser.alarms.onAlarm.addListener((alarm) => callback(alarm.name));
}

currentLocale(): string {
return browser.i18n.getUILanguage();
}
Expand Down
25 changes: 25 additions & 0 deletions src/adapters/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,27 @@ export class WebBrowser extends BaseAdapter {
return BrowserFlavor.Web;
}

private storageListeners: ((
changes: Record<string, { newValue?: unknown }>
) => void)[] = [];

async set<T>(key: string, val: T): Promise<void> {
localStorage.setItem(key, JSON.stringify(val));
const changes = { [key]: { newValue: val } };
this.storageListeners.forEach((cb) => cb(changes));
}
async get<T>(key: string): Promise<T | undefined> {
let s: any;
s = localStorage.getItem(key);
return s && JSON.parse(s);
}

onLocalStorageChanged(
callback: (changes: Record<string, { newValue?: unknown }>) => void
): void {
this.storageListeners.push(callback);
}

async setProxy(_: ProxyConfig): Promise<void> {
window.localStorage.setItem("proxy", JSON.stringify(_));
}
Expand Down Expand Up @@ -128,6 +140,19 @@ export class WebBrowser extends BaseAdapter {
): void {
throw new Error("Method not implemented.");
}
async createPeriodicAlarm(_: string, __: number): Promise<void> {
// no-op for local dev
}
async clearAlarm(_: string): Promise<void> {
// no-op for local dev
}
async getAllAlarmNames(): Promise<string[]> {
return [];
}
onAlarm(_: (name: string) => void): void {
// no-op for local dev
}

currentLocale(): string {
return "en-US";
}
Expand Down
27 changes: 27 additions & 0 deletions src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import {
getCurrentProxySetting,
} from "./services/proxy";
import { WebRequestStatsService } from "./services/stats";
import {
handlePacRefreshAlarm,
reconcilePacAlarms,
refreshActivePacIfStale,
} from "./services/proxy/pacFetcher";
import { onProfileUpdate } from "./services/profile";

// indicator
async function initIndicator() {
Expand All @@ -28,6 +34,27 @@ async function initIndicator() {

initIndicator().catch(console.error);

// Per-profile PAC script refresh. Each PAC profile with a sourceURL and a
// positive refresh interval owns a `pac-refresh:<profileID>` alarm. The
// wake-time refresh only hits the active profile (with a staleness guard) —
// the SW wakes on every main-frame request, so fetching all URL-backed
// profiles here would mean back-to-back PAC requests during normal browsing.
function initPacRefresh() {
Host.onAlarm((name) => {
handlePacRefreshAlarm(name).catch(console.error);
});

onProfileUpdate((profiles) => {
reconcilePacAlarms(profiles).catch(console.error);
});

Promise.all([reconcilePacAlarms(), refreshActivePacIfStale()]).catch(
console.error,
);
}

initPacRefresh();

// proxy auth provider
class ProxyAuthProvider {
// requests[requestID] = request attempts. 0 means the 1st attempt
Expand Down
Loading
Loading