Skip to content

Commit 9a726ea

Browse files
committed
Add configurable lookup providers.
Users can choose a provider from the list, edit or create custom URLs, or leave one/both fields blank to disable the feature.
1 parent e9b02d6 commit 9a726ea

File tree

7 files changed

+297
-68
lines changed

7 files changed

+297
-68
lines changed

src/background.js

Lines changed: 93 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -647,26 +647,18 @@ function lookupOriginMap(origin) {
647647

648648
// Dark mode detection. This can eventually be replaced by
649649
// https://github.com/w3c/webextensions/issues/229
650-
if (typeof window !== 'undefined' && window.matchMedia) {
651-
// Firefox can detect dark mode from the background page.
652-
(async () => {
650+
(async () => {
651+
if (typeof window !== 'undefined' && window.matchMedia) {
652+
// Firefox can detect dark mode from the background page.
653653
await optionsReady;
654654
const query = window.matchMedia('(prefers-color-scheme: dark)');
655655
setColorIsDarkMode(REGULAR_COLOR, query.matches);
656656
query.addEventListener("change", (event) => {
657657
setColorIsDarkMode(REGULAR_COLOR, event.matches);
658658
});
659-
})();
660-
} else {
661-
// Chrome needs an offscreen document to detect dark mode.
662-
chrome.runtime.onMessage.addListener((message) => {
663-
console.log("onMessage", message);
664-
if (message.hasOwnProperty("darkModeOffscreen")) {
665-
setColorIsDarkMode(REGULAR_COLOR, message.darkModeOffscreen);
666-
}
667-
});
668-
669-
(async () => {
659+
} else {
660+
// Chrome needs an offscreen document to detect dark mode.
661+
// See the onMessage handler below.
670662
await optionsReady;
671663
try {
672664
await chrome.offscreen.createDocument({
@@ -684,8 +676,51 @@ if (typeof window !== 'undefined' && window.matchMedia) {
684676
} catch {
685677
// ignore
686678
}
687-
})();
679+
}
680+
})();
681+
682+
chrome.runtime.onMessage.addListener((message) => {
683+
if (message.hasOwnProperty("darkModeOffscreen")) {
684+
setColorIsDarkMode(REGULAR_COLOR, message.darkModeOffscreen);
685+
}
686+
if (message.hasOwnProperty("setStorageSyncDebounce")) {
687+
storageSyncDebouncer.set(message.setStorageSyncDebounce);
688+
}
689+
});
690+
691+
// This class prevents writing to storage.sync more than once per second,
692+
// so the user can type in a text field without spamming the network.
693+
// It runs in background.js to avoid data loss if the user closes the
694+
// options window within 1 second of typing.
695+
class StorageSyncDebouncer {
696+
latest = {};
697+
pending = {};
698+
writePromise = null;
699+
set(items) {
700+
for (let [key, value] of Object.entries(items)) {
701+
if (this.latest[key] !== value) {
702+
this.latest[key] = value;
703+
this.pending[key] = value;
704+
}
705+
}
706+
if (!this.writePromise && Object.keys(this.pending).length > 0) {
707+
this.writePromise = this._writeWithDelay();
708+
}
709+
}
710+
async _writeWithDelay() {
711+
while (Object.keys(this.pending).length > 0) {
712+
const toWrite = this.pending;
713+
this.pending = {};
714+
//console.log("writing", toWrite);
715+
await Promise.all([
716+
chrome.storage.sync.set(toWrite),
717+
new Promise(resolve => setTimeout(resolve, 1000))
718+
]);
719+
}
720+
this.writePromise = null;
721+
}
688722
}
723+
const storageSyncDebouncer = new StorageSyncDebouncer();
689724

690725
// Must "await storageReady;" before reading maps.
691726
// You can force initStorage() from the console for debugging purposes.
@@ -1103,33 +1138,28 @@ chrome.webRequest.onErrorOccurred.addListener(forgetRequest, FILTER_ALL_URLS);
11031138

11041139
// -- contextMenus --
11051140

1106-
// When the user right-clicks an IP address in the popup window, add a menu
1107-
// item to look up the address on bgp.he.net. I don't like picking favorites,
1108-
// so I'm open to making this a config option if someone recommends another
1109-
// useful non-spammy service.
1110-
//
1111-
// Unless http://crbug.com/60758 gets resolved, the context menu's appearance
1112-
// cannot vary based on content.
1141+
// When the user right-clicks a domain or IP address in the popup window,
1142+
// add a menu item that opens the requested lookup provider.
11131143
const MENU_ID = "ipvfoo-lookup";
11141144

1115-
chrome.contextMenus?.removeAll(() => {
1116-
chrome.contextMenus.create({
1117-
title: "Look up on bgp.he.net",
1118-
id: MENU_ID,
1119-
// Scope the menu to text selection in our popup windows.
1120-
contexts: ["selection"],
1121-
documentUrlPatterns: [chrome.runtime.getURL("popup.html")],
1122-
});
1123-
});
1124-
11251145
chrome.contextMenus?.onClicked.addListener((info, tab) => {
11261146
if (info.menuItemId != MENU_ID) return;
1127-
const text = info.selectionText;
1128-
if (IP4_CHARS.test(text) || IP6_CHARS.test(text)) {
1147+
let selectionType = "";
1148+
let text = info.selectionText;
1149+
if (DNS_CHARS.test(text)) {
1150+
selectionType = "domain";
1151+
} else if (IP4_CHARS.test(text) || IP6_CHARS.test(text)) {
1152+
selectionType = "ip";
11291153
// bgp.he.net doesn't support dotted IPv6 addresses.
1130-
chrome.tabs.create({url: `https://bgp.he.net/ip/${reformatForNAT64(text, false)}`});
1131-
} else if (DNS_CHARS.test(text)) {
1132-
chrome.tabs.create({url: `https://bgp.he.net/dns/${text}`});
1154+
text = reformatForNAT64(text, false);
1155+
}
1156+
const provider = options[LOOKUP_PROVIDER];
1157+
const pattern = (provider == "custom") ?
1158+
options[`${CUSTOM_PROVIDER}${selectionType}`] :
1159+
LOOKUP_PROVIDERS[provider]?.[selectionType];
1160+
const url = maybeLookupUrl(pattern, text)?.href;
1161+
if (url) {
1162+
chrome.tabs.create({url});
11331163
} else {
11341164
// Malformed selection; shake the popup content.
11351165
const tabId = /#(\d+)$/.exec(info.pageUrl);
@@ -1158,4 +1188,29 @@ watchOptions(async (optionsChanged) => {
11581188
tabInfo.refreshPageAction();
11591189
}
11601190
}
1161-
});
1191+
1192+
if (optionsChanged.has(LOOKUP_PROVIDER) ||
1193+
optionsChanged.has(CUSTOM_PROVIDER_DOMAIN) ||
1194+
optionsChanged.has(CUSTOM_PROVIDER_IP)) {
1195+
let providerText = options[LOOKUP_PROVIDER];
1196+
if (providerText == "custom") {
1197+
// Show something sensible, even when domain/ip use different providers.
1198+
const hostnames = [
1199+
maybeLookupUrl(options[CUSTOM_PROVIDER_DOMAIN])?.hostname,
1200+
maybeLookupUrl(options[CUSTOM_PROVIDER_IP])?.hostname
1201+
].filter(Boolean);
1202+
providerText = [...new Set(hostnames)].join(" | ");
1203+
}
1204+
chrome.contextMenus?.removeAll(() => {
1205+
if (providerText) {
1206+
chrome.contextMenus.create({
1207+
title: `Lookup on ${providerText}`,
1208+
id: MENU_ID,
1209+
// Scope the menu to text selection in our popup windows.
1210+
contexts: ["selection"],
1211+
documentUrlPatterns: [chrome.runtime.getURL("popup.html")],
1212+
});
1213+
}
1214+
});
1215+
}
1216+
});

src/common.js

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,45 @@ function iconPath(pattern, size, color) {
7070
const REGULAR_COLOR = "regularColorScheme";
7171
const INCOGNITO_COLOR = "incognitoColorScheme";
7272

73+
const LOOKUP_PROVIDER = "lookupProvider";
74+
const CUSTOM_PROVIDER = "customProvider:";
75+
const CUSTOM_PROVIDER_DOMAIN = CUSTOM_PROVIDER + "domain";
76+
const CUSTOM_PROVIDER_IP = CUSTOM_PROVIDER + "ip";
77+
78+
// "$" is a placeholder for the user's selected domain or IP address.
79+
const LOOKUP_PROVIDERS = {
80+
"bgp.he.net": {
81+
domain: "https://bgp.he.net/dns/$",
82+
ip: "https://bgp.he.net/ip/$",
83+
},
84+
"info.addr.tools": {
85+
domain: "https://info.addr.tools/$",
86+
ip: "https://info.addr.tools/$",
87+
},
88+
"ipinfo.io": {
89+
domain: "",
90+
ip: "https://ipinfo.io/$",
91+
},
92+
};
93+
94+
function parseLookupUrl(pattern, placeholder="") {
95+
pattern = pattern?.trim();
96+
if (!pattern) {
97+
return null;
98+
}
99+
if (!/^https:[/][/][^/$]+[/].*[$]/.test(pattern)) {
100+
throw new Error("malformed");
101+
}
102+
return new URL(pattern.replaceAll("$", placeholder)); // may throw
103+
}
104+
function maybeLookupUrl(pattern, placeholder="") {
105+
try {
106+
return parseLookupUrl(pattern, placeholder);
107+
} catch {
108+
return null;
109+
}
110+
}
111+
73112
const NAT64_KEY = "nat64/";
74113
const NAT64_VALIDATE = /^nat64\/[0-9a-f]{24}$/;
75114
const NAT64_DEFAULTS = new Set([
@@ -79,23 +118,34 @@ const NAT64_DEFAULTS = new Set([
79118
]);
80119

81120
let _watchOptionsFunc = null;
82-
const options = {
83-
ready: false,
121+
const DEFAULT_LOCAL_OPTIONS = {
84122
[REGULAR_COLOR]: "darkfg", // default immediately replaced
85123
[INCOGNITO_COLOR]: "lightfg",
124+
};
125+
const DEFAULT_SYNC_OPTIONS = {
126+
[LOOKUP_PROVIDER]: "bgp.he.net",
127+
[CUSTOM_PROVIDER_DOMAIN]: "",
128+
[CUSTOM_PROVIDER_IP]: "",
129+
};
130+
const options = {
131+
ready: false,
86132
[NAT64_KEY]: new Set(NAT64_DEFAULTS),
133+
...DEFAULT_LOCAL_OPTIONS,
134+
...DEFAULT_SYNC_OPTIONS,
87135
};
88136
const optionsReady = (async function() {
89137
const [localItems, syncItems] = await Promise.all(
90138
[chrome.storage.local.get(), chrome.storage.sync.get()]);
91139
for (const [option, value] of Object.entries(localItems)) {
92-
if (option == REGULAR_COLOR || option == INCOGNITO_COLOR) {
140+
if (DEFAULT_LOCAL_OPTIONS.hasOwnProperty(option)) {
93141
options[option] = value;
94142
}
95143
}
96144
for (const [option, value] of Object.entries(syncItems)) {
97145
if (NAT64_VALIDATE.test(option)) {
98146
options[NAT64_KEY].add(option.slice(NAT64_KEY.length));
147+
} else if (DEFAULT_SYNC_OPTIONS.hasOwnProperty(option)) {
148+
options[option] = value;
99149
}
100150
}
101151
options.ready = true;
@@ -107,7 +157,7 @@ chrome.storage.local.onChanged.addListener(function(changes) {
107157
if (!options.ready) return;
108158
const optionsChanged = [];
109159
for (const [option, {oldValue, newValue}] of Object.entries(changes)) {
110-
if (option == REGULAR_COLOR || option == INCOGNITO_COLOR) {
160+
if (DEFAULT_LOCAL_OPTIONS.hasOwnProperty(option)) {
111161
if (options[option] != newValue) {
112162
options[option] = newValue;
113163
optionsChanged.push(option);
@@ -136,6 +186,11 @@ chrome.storage.sync.onChanged.addListener(function(changes) {
136186
if (!optionsChanged.includes(NAT64_KEY)) {
137187
optionsChanged.push(NAT64_KEY);
138188
}
189+
} else if (DEFAULT_SYNC_OPTIONS.hasOwnProperty(option)) {
190+
if (options[option] != newValue) {
191+
options[option] = newValue;
192+
optionsChanged.push(option);
193+
}
139194
}
140195
}
141196
if (optionsChanged.length) {
@@ -206,4 +261,4 @@ function revertNAT64() {
206261
// our local Set is used for deduplication.
207262
_watchOptionsFunc?.([NAT64_KEY]);
208263
}
209-
}
264+
}

src/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "IPvFoo",
33
"manifest_version": 3,
4-
"version": "2.29",
4+
"version": "2.30",
55
"description": "Display the server IP address, with a realtime summary of IPv4, IPv6, and HTTPS information across all page elements.",
66
"homepage_url": "https://github.com/pmarks-net/ipvfoo",
77
"icons": {

src/manifest/chrome-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "IPvFoo",
33
"manifest_version": 3,
4-
"version": "2.29",
4+
"version": "2.30",
55
"description": "Display the server IP address, with a realtime summary of IPv4, IPv6, and HTTPS information across all page elements.",
66
"homepage_url": "https://github.com/pmarks-net/ipvfoo",
77
"icons": {

src/manifest/firefox-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "IPvFoo",
33
"manifest_version": 3,
4-
"version": "2.29",
4+
"version": "2.30",
55
"description": "Display the server IP address, with a realtime summary of IPv4, IPv6, and HTTPS information across all page elements.",
66
"homepage_url": "https://github.com/pmarks-net/ipvfoo",
77
"icons": {

0 commit comments

Comments
 (0)