17 Commits

Author SHA1 Message Date
x 283ebe4dcb Release 2.2 2026-03-07 01:26:27 +01:00
x b488b7576a fix states 2026-03-07 01:24:38 +01:00
x 3fcbab5d30 fix cache 2026-03-07 01:08:31 +01:00
x a755d3f1c2 always return something... 2026-03-07 01:08:26 +01:00
x 9db73ee29c replace system protocol with http/https 2026-03-07 01:07:47 +01:00
x 76758c4572 fix ipv6 ula 2026-03-07 00:59:23 +01:00
x 55e97a68f6 fix copy button with useeffect 2026-03-07 00:57:21 +01:00
x 25d0865f89 fix early return 2026-03-07 00:54:57 +01:00
x 85942e5827 call stuff 2026-03-07 00:54:08 +01:00
x 7048baebc2 localhost failsafe 2026-03-07 00:53:57 +01:00
x b3b76366bf asn failsafe 2026-03-07 00:53:48 +01:00
x c564ac8b69 clean geocache 2026-03-07 00:53:21 +01:00
x 2f16721a93 fix lookup 2026-03-07 00:39:06 +01:00
x 56ed5909c7 fix hostname 2026-03-07 00:27:14 +01:00
x 55b3b61bbf update deps 2026-03-07 00:27:02 +01:00
x 5c107c2f0d Merge branch 'main' of https://github.com/skidoodle/hostinfo 2026-02-24 16:50:02 +01:00
x fea3cf5a62 Release 2.1 2026-02-24 16:49:50 +01:00
12 changed files with 187 additions and 72 deletions
+17 -29
View File
@@ -5,21 +5,19 @@
"": {
"name": "hostinfo",
"dependencies": {
"@heroicons/react": "^2.2.0",
"@tailwindcss/vite": "^4.2.1",
"clsx": "^2.1.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1",
"@heroicons/react": "latest",
"@tailwindcss/vite": "latest",
"react": "latest",
"react-dom": "latest",
"tailwindcss": "latest",
},
"devDependencies": {
"@types/chrome": "^0.1.37",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@wxt-dev/module-react": "^1.1.5",
"typescript": "^5.9.3",
"wxt": "^0.20.18",
"@types/chrome": "latest",
"@types/react": "latest",
"@types/react-dom": "latest",
"@wxt-dev/module-react": "latest",
"typescript": "latest",
"wxt": "latest",
},
},
},
@@ -268,7 +266,7 @@
"@wxt-dev/browser": ["@wxt-dev/browser@0.1.37", "", { "dependencies": { "@types/filesystem": "*", "@types/har-format": "*" } }, "sha512-I32XWCNRy2W6UgbaVXz8BHGBGtm8urGRRBrcNLagUBXTrBi7wCE6zWePUvvK+nUl7qUCZ7iQ1ufdP0c1DEWisw=="],
"@wxt-dev/module-react": ["@wxt-dev/module-react@1.1.5", "", { "dependencies": { "@vitejs/plugin-react": "^4.4.1 || ^5.0.0" }, "peerDependencies": { "wxt": ">=0.19.16" } }, "sha512-KgsUrsgH5rBT8MwiipnDEOHBXmLvTIdFICrI7KjngqSf9DpVRn92HsKmToxY0AYpkP19hHWta2oNYFTzmmm++g=="],
"@wxt-dev/module-react": ["@wxt-dev/module-react@1.2.1", "", { "dependencies": { "@vitejs/plugin-react": "^4.4.1 || ^5.0.0" }, "peerDependencies": { "vite": "^5.4.19 || ^6.3.4 || ^7.0.0 || ^8.0.0-0", "wxt": ">=0.19.16" } }, "sha512-NkXhXP1KqbTmKuQ7LwESFUnQDxQRiHw98ZQ6cXKuulRvyxtyCfc6gOqbKMHPP9bp497UVHaozr3ZQj1lvcPPTQ=="],
"@wxt-dev/storage": ["@wxt-dev/storage@1.2.6", "", { "dependencies": { "@wxt-dev/browser": "^0.1.4", "async-mutex": "^0.5.0", "dequal": "^2.0.3" } }, "sha512-f6AknnpJvhNHW4s0WqwSGCuZAj0fjP3EVNPBO5kB30pY+3Zt/nqZGqJN6FgBLCSkYjPJ8VL1hNX5LMVmvxQoDw=="],
@@ -344,8 +342,6 @@
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
@@ -684,7 +680,7 @@
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"pino": ["pino@9.7.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg=="],
@@ -810,8 +806,6 @@
"stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="],
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
@@ -906,6 +900,8 @@
"@aklinker1/rollup-plugin-visualizer/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
"@aklinker1/rollup-plugin-visualizer/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="],
@@ -966,6 +962,8 @@
"log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"multimatch/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
@@ -990,18 +988,8 @@
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"unimport/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"unplugin/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"unplugin-utils/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"update-notifier/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"vite/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
+8 -1
View File
@@ -3,12 +3,19 @@ import { CheckIcon, ClipboardDocumentIcon } from '@heroicons/react/24/outline';
export const CopyButton = ({ text }: { text: string }) => {
const [copied, setCopied] = useState(false);
useEffect(() => {
let timeout: ReturnType<typeof setTimeout>;
if (copied) {
timeout = setTimeout(() => setCopied(false), 2000);
}
return () => clearTimeout(timeout);
}, [copied]);
const handleCopy = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
+2 -2
View File
@@ -23,14 +23,14 @@ export const PublicNetworkView = ({ data, domain }: { data: GeoData, domain: str
<InfoRow
icon={GlobeAltIcon}
label="Hostname"
value={domain}
value={data.hostname || 'N/A'}
canCopy
iconColor="text-indigo-500"
/>
<InfoRow
icon={MapPinIcon}
label="Location"
value={data.countryName || 'Unknown Location'}
value={data.countryName || 'N/A'}
iconColor="text-emerald-500"
/>
<InfoRow
+81 -17
View File
@@ -3,11 +3,6 @@ import { GeoService } from '@/services/geo';
import { IconService } from '@/services/icon';
import { DnsService } from '@/services/dns';
const SYSTEM_PROTOCOLS = [
'chrome:', 'about:', 'edge:', 'moz-extension:',
'chrome-extension:', 'file:', 'view-source:', 'data:', 'devtools:'
];
const tabStates = new Map<number, TabState>();
function getDomain(url: string) {
@@ -19,8 +14,8 @@ function getDomain(url: string) {
}
function applyIconForState(tabId: number, state: TabState) {
const isSystem = SYSTEM_PROTOCOLS.some(p => state.url.startsWith(p));
if (isSystem) {
const isWebProtocol = state.url.startsWith('http://') || state.url.startsWith('https://');
if (!isWebProtocol) {
IconService.updateIcon(tabId, null, true);
} else if (state.status === 'success' && state.data) {
let code = state.data.countryCode;
@@ -33,7 +28,7 @@ function applyIconForState(tabId: number, state: TabState) {
async function initTab(tabId: number, url: string, resolveDns = false) {
if (!url) return;
const isSystem = SYSTEM_PROTOCOLS.some(p => url.startsWith(p));
const isWebProtocol = url.startsWith('http://') || url.startsWith('https://');
const domain = getDomain(url);
let currentState = tabStates.get(tabId);
@@ -63,26 +58,41 @@ async function initTab(tabId: number, url: string, resolveDns = false) {
const newState: TabState = {
url,
domain,
status: isSystem || hasExistingData ? 'success' : 'loading',
status: !isWebProtocol || hasExistingData ? 'success' : 'loading',
data: hasExistingData ? currentState!.data : null,
errorMessage: null,
lastUpdated: Date.now()
};
tabStates.set(tabId, newState);
StorageService.setTabState(tabId, newState).catch(() => { });
await StorageService.setTabState(tabId, newState).catch(() => { });
applyIconForState(tabId, newState);
if (!isSystem && !hasExistingData && resolveDns) {
if (!isWebProtocol && !hasExistingData) {
const performDnsFallback = async () => {
const state = tabStates.get(tabId);
if (state?.status !== 'loading' || state.url !== url) return;
const ip = await DnsService.resolve(domain);
const stateAfterDns = tabStates.get(tabId);
if (stateAfterDns?.status !== 'loading' || stateAfterDns.url !== url) return;
if (ip) {
await processIp(tabId, url, ip);
} else {
await updateState(tabId, {
status: 'error',
errorMessage: 'Could not resolve host'
});
}, url);
}
};
if (resolveDns) {
await performDnsFallback();
} else {
setTimeout(performDnsFallback, 1500);
}
}
}
@@ -127,18 +137,19 @@ async function processIp(tabId: number, url: string, ip: string) {
};
tabStates.set(tabId, newState);
await StorageService.setTabState(tabId, newState);
await StorageService.setTabState(tabId, newState).catch(() => { });
applyIconForState(tabId, newState);
}
async function updateState(tabId: number, updates: Partial<TabState>) {
async function updateState(tabId: number, updates: Partial<TabState>, expectedUrl?: string) {
let current = tabStates.get(tabId);
if (!current) {
current = await StorageService.getTabState(tabId) || undefined;
}
if (current) {
if (expectedUrl && current.url !== expectedUrl) return;
const newState = { ...current, ...updates };
tabStates.set(tabId, newState);
await StorageService.setTabState(tabId, newState);
@@ -148,6 +159,32 @@ async function updateState(tabId: number, updates: Partial<TabState>) {
export default defineBackground({
main() {
browser.runtime.onStartup.addListener(() => {
StorageService.cleanExpiredGeoCache().catch(console.error);
});
browser.runtime.onInstalled.addListener(() => {
StorageService.cleanExpiredGeoCache().catch(console.error);
});
browser.alarms.create('cleanup-geo-cache', { periodInMinutes: 1440 });
browser.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'cleanup-geo-cache') {
StorageService.cleanExpiredGeoCache().catch(console.error);
}
});
browser.tabs.onReplaced.addListener((addedTabId, removedTabId) => {
tabStates.delete(removedTabId);
StorageService.removeTabState(removedTabId);
browser.tabs.get(addedTabId).then((tab) => {
if (tab.url) {
initTab(tab.id!, tab.url, true)
}
}).catch(() => { })
})
browser.webNavigation.onBeforeNavigate.addListener((details) => {
if (details.frameId !== 0) return;
initTab(details.tabId, details.url, false);
@@ -194,13 +231,16 @@ export default defineBackground({
}
const ip = await DnsService.resolve(hostname);
const currentState = tabStates.get(details.tabId);
if (currentState?.status !== 'loading' || currentState.url !== details.url) return;
if (ip) {
await processIp(details.tabId, details.url, ip);
} else {
await updateState(details.tabId, {
status: 'error',
errorMessage: 'Could not resolve host'
});
}, details.url);
}
}
});
@@ -208,12 +248,23 @@ export default defineBackground({
browser.webRequest.onErrorOccurred.addListener(
async (details) => {
if (details.type !== 'main_frame') return;
if (details.error === 'net::ERR_ABORTED') return;
if (details.error === 'net::ERR_ABORTED') {
try {
const tab = await browser.tabs.get(details.tabId);
if (tab.url) {
const currentState = tabStates.get(details.tabId);
if (currentState && currentState.url !== tab.url) {
initTab(tab.id!, tab.url, true);
}
}
} catch { }
return;
}
await updateState(details.tabId, {
status: 'error',
errorMessage: details.error
});
}, details.url);
},
{ urls: ['<all_urls>'] }
);
@@ -229,6 +280,9 @@ export default defineBackground({
const state = tabStates.get(tab.id!);
if (state) {
applyIconForState(tab.id!, state);
if (state.status === 'loading' && Date.now() - state.lastUpdated > 2000) {
initTab(tab.id!, tab.url, true)
}
} else {
initTab(tab.id!, tab.url, true);
}
@@ -237,9 +291,19 @@ export default defineBackground({
browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status && tab.url) {
if (tab.url) {
const state = tabStates.get(tabId);
if (state) {
applyIconForState(tabId, state);
if (!state || state.url !== tab.url) {
initTab(tabId, tab.url, true)
} else {
applyIconForState(tabId, state);
if (changeInfo.status === 'complete' && state.status === 'loading') {
initTab(tabId, tab.url, true)
}
}
}
}
}
});
+15 -1
View File
@@ -8,7 +8,10 @@ export function useHostInfo() {
const fetchInfo = async () => {
try {
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
if (!tab?.id) return;
if (!tab?.id) {
if (isMounted) setLoading(false);
return;
}
const data = await StorageService.getTabState(tab.id);
if (data) {
@@ -16,6 +19,9 @@ export function useHostInfo() {
setInfo(data);
setLoading(false);
}
if (data.status === 'loading' && Date.now() - data.lastUpdated > 2000) {
browser.runtime.sendMessage({ type: 'INIT_TAB', tabId: tab.id, url: tab.url })
}
} else {
if (tab.url) {
await browser.runtime.sendMessage({ type: 'INIT_TAB', tabId: tab.id, url: tab.url });
@@ -32,8 +38,16 @@ export function useHostInfo() {
const listener = (changes: any, areaName: string) => {
if (areaName === 'session' || areaName === 'local') {
browser.tabs.query({ active: true, currentWindow: true }).then(([tab]) => {
if (tab?.id) {
const sessionKey = `tab_${tab.id}`;
const localKey = `session_tab_${tab.id}`;
if (changes[sessionKey] || changes[localKey]) {
fetchInfo();
}
}
});
}
};
browser.storage.onChanged.addListener(listener);
+2 -2
View File
@@ -2,7 +2,7 @@
"name": "hostinfo",
"description": "Receive information of a domain directly in the browser when browsing a website",
"private": true,
"version": "2.0.0",
"version": "2.2",
"type": "module",
"scripts": {
"dev": "wxt",
@@ -25,7 +25,7 @@
"@types/chrome": "^0.1.37",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@wxt-dev/module-react": "^1.1.5",
"@wxt-dev/module-react": "^1.2.1",
"typescript": "^5.9.3",
"wxt": "^0.20.18"
}
+8 -1
View File
@@ -1,13 +1,20 @@
export const DnsService = {
async resolve(hostname: string): Promise<string | null> {
if (hostname === 'localhost' || hostname.endsWith('.local')) {
return '127.0.0.1';
}
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const response = await fetch(
`https://cloudflare-dns.com/dns-query?name=${hostname}&type=A`,
{
headers: { accept: 'application/dns-json' },
credentials: 'omit'
credentials: 'omit',
signal: controller.signal,
}
);
clearTimeout(timeout);
if (!response.ok) return null;
+16 -4
View File
@@ -13,11 +13,15 @@ export const GeoService = {
if (cached) return cached;
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const res = await fetch(`https://ip.albert.lol/${ip}`, {
method: 'GET',
cache: 'force-cache',
credentials: 'omit'
credentials: 'omit',
signal: controller.signal,
});
clearTimeout(timeout);
if (!res.ok) throw new Error(`API Error ${res.status}`);
@@ -28,8 +32,9 @@ export const GeoService = {
return data;
} catch (error) {
console.warn('Geo lookup failed for', ip, error);
return {
const failedData: GeoData = {
ip,
hostname: null,
countryCode: null,
countryName: 'Unknown',
city: null,
@@ -40,12 +45,16 @@ export const GeoService = {
isLocal: false,
isBogon: false
};
await StorageService.setGeoCache(ip, failedData);
return failedData;
}
},
getLocalData(ip: string): GeoData {
return {
ip,
hostname: null,
countryCode: null,
countryName: 'Local Network',
city: null,
@@ -59,17 +68,20 @@ export const GeoService = {
},
transform(ip: string, apiData: any): GeoData {
const asnMatch = apiData.org?.match(/^AS\d+/i);
return {
ip,
hostname: apiData.hostname || null,
countryCode: apiData.country || null,
countryName: apiData.country ? (codes[apiData.country.toLowerCase()] || apiData.country) : null,
city: apiData.city || null,
region: apiData.region || null,
org: apiData.org || null,
asn: apiData.org?.split(' ')[0] || null,
asn: asnMatch ? asnMatch[0].toUpperCase() : null,
timezone: apiData.timezone || null,
isLocal: false,
isBogon: apiData.bogon || false
};
}
};
}
+5 -3
View File
@@ -1,4 +1,4 @@
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,3}(:[0-9a-fA-F]{1,4}){1,4})|([0-9a-fA-F]{1,2}(:[0-9a-fA-F]{1,4}){1,5})|([0-9a-fA-F]{1,4}:)((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
export const IpUtils = {
@@ -13,8 +13,10 @@ export const IpUtils = {
const clean = ip.replace(/^\[|\]$/g, '');
if (clean === '::1' || clean === '::') return true;
if (clean.toLowerCase().startsWith('fe80:')) return true;
if (clean.toLowerCase().startsWith('fc00:')) return true;
const lowerClean = clean.toLowerCase();
if (lowerClean.startsWith('fe80:')) return true;
if (lowerClean.startsWith('fc') || lowerClean.startsWith('fd')) return true;
if (clean.includes('.')) {
const parts = clean.split('.').map(Number);
+19
View File
@@ -7,6 +7,7 @@ export const StorageService = {
try {
const res = await browser.storage.session.get(key);
if (res[key]) return res[key] as TabState;
return null;
} catch { }
}
const res = await browser.storage.local.get(`session_${key}`);
@@ -53,5 +54,23 @@ export const StorageService = {
const key = `geo_${ip.replace(/:/g, '_')}`;
const entry: CacheEntry = { data, timestamp: Date.now() };
await browser.storage.local.set({ [key]: entry });
},
async cleanExpiredGeoCache(): Promise<void> {
const res = await browser.storage.local.get(null);
const keysToRemove: string[] = [];
for (const [key, value] of Object.entries(res)) {
if (key.startsWith('geo_')) {
const entry = value as CacheEntry;
if (Date.now() - entry.timestamp > CACHE_TTL) {
keysToRemove.push(key);
}
}
}
if (keysToRemove.length > 0) {
await browser.storage.local.remove(keysToRemove);
}
}
};
+1
View File
@@ -1,5 +1,6 @@
export interface GeoData {
ip: string;
hostname: string | null;
countryCode: string | null;
countryName: string | null;
city: string | null;
+3 -2
View File
@@ -7,12 +7,13 @@ export default defineConfig({
manifest: {
name: 'Host Info',
description: 'Get host information',
version: '2.0.0',
version: '2.2',
permissions: [
'tabs',
'webRequest',
'webNavigation',
'storage'
'storage',
'alarms'
],
host_permissions: [
'<all_urls>',