25 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
x e9315bec47 Update showcase image in README
Updated the showcase image and adjusted its width.
2026-02-24 16:23:19 +01:00
x 7d614a2a7e Bug fixes and performance improvements 2026-02-24 16:05:02 +01:00
x da23868817 Add host info views and network error handling
Introduce modular host info UI components: Header, InfoRow,
CopyButton, and Browser/Local/Public views. Refactor ServerInfo to
compose these components.

Add network error handling: background listens for webRequest
onErrorOccurred and forwards errors to Tab.handleError. Implement
Tab.handleError to store friendly error info and Tab.processSystemPage
to handle browser/system pages.
2026-02-03 05:22:28 +01:00
x 9292a4a6e2 Refactor ServerInfo and add icon and tab fallbacks 2026-02-03 05:03:55 +01:00
x 2973e038d6 half way there 2026-02-03 04:24:11 +01:00
x 991cd10f40 remove author email from configuration 2025-03-24 20:27:45 +01:00
x 6bf00dc6e0 The extension ID is required in Manifest Version 3 and above. 2025-03-24 20:26:11 +01:00
x a4ae08870d fix firefox icon problem ((mv3)) 2025-03-24 20:19:31 +01:00
538 changed files with 1344 additions and 1064 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
A browser extension built with [WXT.dev](https://wxt.dev) and React that lets you discover the origin of the website you're visiting. With a single click, you can view detailed information such as the **country of origin**, **IP address**, **ASN (Autonomous System Number)**, and more. You can also quickly search for the website's details on [Censys](https://censys.io) for deeper insights. A browser extension built with [WXT.dev](https://wxt.dev) and React that lets you discover the origin of the website you're visiting. With a single click, you can view detailed information such as the **country of origin**, **IP address**, **ASN (Autonomous System Number)**, and more. You can also quickly search for the website's details on [Censys](https://censys.io) for deeper insights.
<img src="https://github.com/user-attachments/assets/83a6316c-54b8-41a8-8d43-c794a5f62696" alt="Showcase" width="200"/> <img width="420" alt="Showcase" src="https://github.com/user-attachments/assets/7248747d-3216-4d48-8060-f7627bfd8762" />
<a href="https://addons.mozilla.org/addon/hostinfo/"><img src="https://github.com/user-attachments/assets/4e69214c-c11a-4202-919a-fac7d58dbb55" alt="Get hostinfo for Firefox"></a> <a href="https://addons.mozilla.org/addon/hostinfo/"><img src="https://github.com/user-attachments/assets/4e69214c-c11a-4202-919a-fac7d58dbb55" alt="Get hostinfo for Firefox"></a>
<a href="https://chromewebstore.google.com/detail/hostinfo/ehleblniighmnfhfimcbfhmdpdhamcbp"><img src="https://github.com/user-attachments/assets/4bf31178-6244-467c-916d-79e926dec379" alt="Get hostinfo for Chrome"></a> <a href="https://chromewebstore.google.com/detail/hostinfo/ehleblniighmnfhfimcbfhmdpdhamcbp"><img src="https://github.com/user-attachments/assets/4bf31178-6244-467c-916d-79e926dec379" alt="Get hostinfo for Chrome"></a>
+362 -590
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
import { CpuChipIcon, GlobeAltIcon } from '@heroicons/react/24/outline';
import { Header } from './Header';
import { InfoRow } from './Info';
export const BrowserResourceView = ({ url }: { url: string }) => {
return (
<div className="w-80 bg-white dark:bg-gray-950 font-sans">
<Header title="System Resource" flagCode={null} />
<div className="p-5">
<InfoRow
icon={CpuChipIcon}
label="Type"
value="Local Browser Page"
iconColor="text-orange-500"
/>
<InfoRow
icon={GlobeAltIcon}
label="URL"
value={url}
iconColor="text-gray-400"
/>
</div>
<div className="px-5 pb-5 text-xs text-gray-400 text-center">
This page is generated locally by your browser.
</div>
</div>
);
};
+30
View File
@@ -0,0 +1,30 @@
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);
};
return (
<button
onClick={handleCopy}
className="ml-2 p-1 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-100 dark:hover:text-gray-200 dark:hover:bg-gray-800 transition-all opacity-0 group-hover:opacity-100 focus:opacity-100 cursor-pointer"
title="Copy to clipboard"
>
{copied ? <CheckIcon className="w-3.5 h-3.5 text-green-500" /> : <ClipboardDocumentIcon className="w-3.5 h-3.5" />}
</button>
);
};
+7 -10
View File
@@ -1,16 +1,13 @@
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline';
export default function Error({ error }: { error: string }) { export default function Error({ error }: { error: string }) {
return ( return (
<div className="min-w-[300px] bg-gray-900 shadow-2xl p-6 text-white font-sans"> <div className="w-[320px] bg-white dark:bg-gray-950 flex flex-col items-center justify-center p-8 text-center font-sans">
<div className="flex items-center justify-between mb-6"> <div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-full mb-4">
<h2 className="text-2xl font-bold bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent"> <ExclamationTriangleIcon className="w-6 h-6 text-red-600 dark:text-red-400" />
Error
</h2>
</div>
<div className="flex items-center space-x-3">
<div>
<p className="text-sm text-gray-300">{error}</p>
</div>
</div> </div>
<h3 className="text-sm font-bold text-gray-900 dark:text-white mb-1">Unable to Load</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">{error}</p>
</div> </div>
); );
} }
+29
View File
@@ -0,0 +1,29 @@
export const Header = ({ title, flagCode }: { title: string, flagCode?: string | null }) => {
const getFlagUrl = (code?: string | null) => {
if (!code) return '';
try {
const path = `/${code.toLowerCase()}.png`;
return browser.runtime.getURL(path as any);
} catch {
return '';
}
};
return (
<div className="px-5 py-4 bg-gray-50/50 dark:bg-gray-900/50 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between">
<div className="min-w-0 pr-3">
<h1 className="text-base font-bold text-gray-900 dark:text-white truncate" title={title}>
{title}
</h1>
</div>
{flagCode && (
<img
src={getFlagUrl(flagCode)}
alt={flagCode}
className="w-8 h-auto rounded shadow-sm border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800"
onError={(e) => (e.currentTarget.style.display = 'none')}
/>
)}
</div>
);
};
+45
View File
@@ -0,0 +1,45 @@
import { CopyButton } from './CopyButton';
export const InfoRow = ({
icon: Icon,
label,
value,
href,
canCopy,
iconColor = "text-gray-400 dark:text-gray-500"
}: {
icon: any,
label: string,
value: string | null,
href?: string,
canCopy?: boolean,
iconColor?: string
}) => {
if (!value) return null;
return (
<div className="group flex items-start py-3 border-b border-gray-100 dark:border-gray-800 last:border-0">
<div className={`mt-0.5 mr-3 ${iconColor}`}>
<Icon className="w-4 h-4" />
</div>
<div className="flex-1 min-w-0">
<p className="text-[10px] uppercase tracking-wider text-gray-400 dark:text-gray-500 font-semibold mb-0.5">{label}</p>
<div className="flex items-center">
{href ? (
<a
href={href}
target="_blank"
rel="noreferrer"
className="text-sm font-medium text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 truncate transition-colors flex items-center gap-1.5"
>
<span className="truncate">{value}</span>
</a>
) : (
<span className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate select-all">{value}</span>
)}
{canCopy && <CopyButton text={value} />}
</div>
</div>
</div>
);
};
+30
View File
@@ -0,0 +1,30 @@
import { CpuChipIcon, ServerIcon } from '@heroicons/react/24/outline';
import { Header } from './Header';
import { InfoRow } from './Info';
import type { GeoData } from '@/utils/types';
export const LocalNetworkView = ({ data, domain }: { data: GeoData, domain: string }) => {
return (
<div className="w-80 bg-white dark:bg-gray-950 font-sans">
<Header
title={domain}
flagCode="unknown"
/>
<div className="p-5">
<InfoRow
icon={CpuChipIcon}
label="Type"
value="Local / Private Network"
iconColor="text-orange-500"
/>
<InfoRow
icon={ServerIcon}
label="IP Address"
value={data.ip}
canCopy
iconColor="text-blue-500"
/>
</div>
</div>
);
};
+58
View File
@@ -0,0 +1,58 @@
import { ServerIcon, MapPinIcon, GlobeAltIcon, BuildingOfficeIcon } from '@heroicons/react/24/outline';
import { Header } from './Header';
import { InfoRow } from './Info';
import type { GeoData } from '@/utils/types';
export const PublicNetworkView = ({ data, domain }: { data: GeoData, domain: string }) => {
return (
<div className="w-80 bg-white dark:bg-gray-950 font-sans text-gray-900 dark:text-gray-100">
<Header
title="Host Information"
flagCode={data.countryCode}
/>
<div className="p-5 space-y-0.5">
<InfoRow
icon={ServerIcon}
label="IP Address"
value={data.ip}
href={`https://ip.albert.lol/${data.ip}`}
canCopy
iconColor="text-blue-500"
/>
<InfoRow
icon={GlobeAltIcon}
label="Hostname"
value={data.hostname || 'N/A'}
canCopy
iconColor="text-indigo-500"
/>
<InfoRow
icon={MapPinIcon}
label="Location"
value={data.countryName || 'N/A'}
iconColor="text-emerald-500"
/>
<InfoRow
icon={BuildingOfficeIcon}
label="Organization / ASN"
value={data.org}
href={data.asn ? `https://bgp.he.net/${data.asn}` : undefined}
iconColor="text-violet-500"
/>
</div>
<div className="px-5 pb-5 pt-2">
<a
href={`https://search.censys.io/hosts/${data.ip}`}
target="_blank"
rel="noreferrer"
className="flex items-center justify-center w-full py-2 px-4 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-md transition-all text-xs font-medium shadow-sm hover:shadow cursor-pointer"
>
<GlobeAltIcon className="w-3.5 h-3.5 mr-2 text-gray-400" />
Analyze on Censys
</a>
</div>
</div>
);
};
+20 -99
View File
@@ -1,110 +1,31 @@
import { LinkIcon, ServerIcon, IdentificationIcon, MapPinIcon } from '@heroicons/react/24/outline'; import type { TabState } from '@/utils/types';
import { codes } from '@/utils/codes'; import Error from './Error';
import { BrowserResourceView } from './Browser';
import { LocalNetworkView } from './Local';
import { PublicNetworkView } from './Public';
export default function ServerInfo({ data }: { data: ServerData }) { export default function ServerInfo({ state }: { state: TabState }) {
const countryName = data.country if (state.status === 'error') {
? codes[data.country.toLowerCase()] || "N/A" return <Error error={state.errorMessage || 'Unknown Error'} />;
: "N/A"; }
if (data.isBrowserResource) { if (state.status === 'success' && !state.data) {
return <BrowserResourceView url={state.url} />;
}
if (state.status === 'loading' || !state.data) {
return ( return (
<div className="min-w-[300px] bg-gray-900 shadow-2xl p-6 text-white font-sans"> <div className="w-80 h-64 flex flex-col items-center justify-center space-y-3 bg-white dark:bg-gray-950">
<div className="flex items-center justify-between mb-6"> <div className="w-6 h-6 border-2 border-gray-200 dark:border-gray-700 border-t-blue-600 rounded-full animate-spin"></div>
<h2 className="text-2xl font-bold bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent"> <span className="text-xs text-gray-400 font-medium">Analyzing Network...</span>
Browser Resource
</h2>
</div>
<div className="flex items-center space-x-3">
<div>
<p className="text-sm text-gray-300">The requested document was obtained from the local computer</p>
</div>
</div>
</div> </div>
); );
} }
if (data.isLocal) { if (state.data.isLocal || state.data.isBogon) {
return ( return <LocalNetworkView data={state.data} domain={state.domain} />;
<div className="min-w-[300px] bg-gray-900 shadow-2xl p-6 text-white font-sans">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">
Internal Network
</h2>
</div>
{data.ip && (
<div className="flex items-center space-x-3">
<ServerIcon className="w-6 h-6 text-yellow-400 flex-shrink-0" />
<div>
<p className="text-sm text-gray-400">IP Address</p>
<p className="font-medium">{data.ip}</p>
</div>
</div>
)}
{data.hostname && (
<div className="flex items-center space-x-3">
<LinkIcon className="w-6 h-6 text-green-400 flex-shrink-0" />
<div>
<p className="text-sm text-gray-400">Hostname</p>
<p className="font-medium break-all">{data.hostname}</p>
</div>
</div>
)}
</div>
);
} }
return ( return <PublicNetworkView data={state.data} domain={state.domain} />;
<div className="min-w-[300px] bg-gray-900 shadow-2xl p-6 text-white font-sans">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">
Host Information
</h2>
</div>
<div className="space-y-4">
<div className="flex items-center space-x-3">
<ServerIcon className="w-6 h-6 text-yellow-400 flex-shrink-0" />
<div>
<p className="text-sm text-gray-400">IP Address</p>
<p className="font-medium hover:underline"><a href={`https://ip.albert.lol/${data.ip}`} target='_blank'>{data.ip}</a></p>
</div>
</div>
<div className="flex items-center space-x-3">
<LinkIcon className="w-6 h-6 text-green-400 flex-shrink-0" />
<div>
<p className="text-sm text-gray-400">Hostname</p>
<p className="font-medium break-all">{data.hostname}</p>
</div>
</div>
<div className="flex items-center space-x-3">
<MapPinIcon className="w-6 h-6 text-blue-400 flex-shrink-0" />
<div>
<p className="text-sm text-gray-400">Location</p>
<p className="font-medium">{countryName}</p>
</div>
</div>
<div className="flex items-center space-x-3">
<IdentificationIcon className="w-6 h-6 text-red-400 flex-shrink-0" />
<div>
<p className="text-sm text-gray-400">Org</p>
<p className="font-medium hover:underline">
<a href={`https://bgp.he.net/${data.org.split(' ')[0]}`} target='_blank'>{data.org}</a>
</p>
</div>
</div>
</div>
<div className="mt-6 pt-4 border-t border-gray-700">
<p className="text-xs text-gray-400 text-center hover:underline">
<a href={`https://search.censys.io/search?resource=hosts&sort=RELEVANCE&per_page=25&virtual_hosts=EXCLUDE&q=${data.origin}`} target='_blank'>Search on Censys</a>
</p>
</div>
</div>
);
} }
+300 -160
View File
@@ -1,177 +1,317 @@
import psl from 'psl' import { StorageService } from '@/utils/storage';
import { GeoService } from '@/services/geo';
import { IconService } from '@/services/icon';
import { DnsService } from '@/services/dns';
let currentTabUrl: string | null = null const tabStates = new Map<number, TabState>();
async function resolveARecord(hostname: string): Promise<string | null> { function getDomain(url: string) {
try { try {
let dnsResponse = await fetch( return new URL(url).hostname;
`https://cloudflare-dns.com/dns-query?name=${hostname}&type=A`, } catch {
{ headers: { Accept: 'application/dns-json' } } return url;
)
if (dnsResponse.ok) {
const dnsData = await dnsResponse.json()
const aRecord = dnsData.Answer?.find(
(entry: DNSEntry) => entry.type === 1
)?.data
if (aRecord) return aRecord
}
dnsResponse = await fetch(
`https://cloudflare-dns.com/dns-query?name=${hostname}&type=AAAA`,
{ headers: { Accept: 'application/dns-json' } }
)
if (dnsResponse.ok) {
const dnsData = await dnsResponse.json()
const aaaaRecord = dnsData.Answer?.find(
(entry: DNSEntry) => entry.type === 28
)?.data
if (aaaaRecord) return aaaaRecord
}
return null
} catch (error) {
console.error('Failed to fetch DNS data:', error)
return null
} }
} }
function isIP(host: string): boolean { function applyIconForState(tabId: number, state: TabState) {
const cleanedHost = host.replace(/^\[|\]$/g, '') const isWebProtocol = state.url.startsWith('http://') || state.url.startsWith('https://');
if (!isWebProtocol) {
const ipv4Regex = IconService.updateIcon(tabId, null, true);
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ } else if (state.status === 'success' && state.data) {
let code = state.data.countryCode;
const ipv6Regex = if (state.data.asn === 'AS13335') code = 'cloudflare';
/(([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,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){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]))/ IconService.updateIcon(tabId, code, state.data.isLocal);
} else {
return ipv4Regex.test(cleanedHost) || ipv6Regex.test(cleanedHost) IconService.updateIcon(tabId, null, false);
}
async function getIPInfo(host: string): Promise<any | null> {
const cleanedHost = host.replace(/^\[|\]$/g, '')
if (isIP(cleanedHost)) {
const response = await fetch(`https://ip.albert.lol/${cleanedHost}`)
const data = await response.json()
return data.ip ? data : null
}
const resolvedIp = await resolveARecord(cleanedHost)
if (!resolvedIp) return null
const response = await fetch(`https://ip.albert.lol/${resolvedIp}`)
return await response.json()
}
async function handleTabUpdate(url: string) {
if (url === currentTabUrl) return
currentTabUrl = url
try {
const hostname = new URL(url).hostname
const apiData = await getIPInfo(hostname)
if (!apiData || !apiData.ip) {
await updateIcon(null)
return
}
if (apiData.bogon === true) {
await updateIcon(null)
return
}
const country = apiData.country || null
const asn = apiData.org?.split(' ')[0]
let iconCode = country
if (!iconCode && asn === 'AS13335') {
iconCode = 'cloudflare'
}
await updateIcon(iconCode)
} catch (error) {
console.error('Failed to handle tab update:', error)
await updateIcon(null)
} }
} }
browser.tabs.onActivated.addListener(async activeInfo => { async function initTab(tabId: number, url: string, resolveDns = false) {
const tab = await browser.tabs.get(activeInfo.tabId) if (!url) return;
if (tab.url) await handleTabUpdate(tab.url) const isWebProtocol = url.startsWith('http://') || url.startsWith('https://');
}) const domain = getDomain(url);
browser.tabs.onUpdated.addListener(async (_tabId, changeInfo) => { let currentState = tabStates.get(tabId);
if (changeInfo.url) await handleTabUpdate(changeInfo.url)
}) if (!currentState || currentState.url !== url) {
tabStates.set(tabId, {
url,
domain,
status: 'loading',
data: null,
errorMessage: null,
lastUpdated: Date.now()
});
}
if (!currentState) {
currentState = await StorageService.getTabState(tabId) || undefined;
}
const latestState = tabStates.get(tabId);
if (latestState && latestState.url !== url) return;
if (latestState && latestState.status === 'success' && latestState.data) return;
const isSameDomain = currentState?.domain === domain;
const hasExistingData = isSameDomain && !!currentState?.data;
const newState: TabState = {
url,
domain,
status: !isWebProtocol || hasExistingData ? 'success' : 'loading',
data: hasExistingData ? currentState!.data : null,
errorMessage: null,
lastUpdated: Date.now()
};
tabStates.set(tabId, newState);
await StorageService.setTabState(tabId, newState).catch(() => { });
applyIconForState(tabId, newState);
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);
}
}
}
async function processIp(tabId: number, url: string, ip: string) {
let current = tabStates.get(tabId);
if (!current) {
current = await StorageService.getTabState(tabId) || undefined;
}
const latestState1 = tabStates.get(tabId);
if (latestState1) {
try {
if (new URL(latestState1.url).hostname !== new URL(url).hostname) return;
} catch {
return;
}
}
if (current?.status === 'success' && current.data?.ip === ip) {
return;
}
const geoData = await GeoService.getGeoData(ip);
const stateAfterFetch = tabStates.get(tabId);
if (stateAfterFetch) {
try {
if (new URL(stateAfterFetch.url).hostname !== new URL(url).hostname) return;
} catch {
return;
}
}
const newState: TabState = {
url: stateAfterFetch?.url || url,
domain: stateAfterFetch?.domain || getDomain(url),
status: 'success',
data: geoData,
errorMessage: null,
lastUpdated: Date.now()
};
tabStates.set(tabId, newState);
await StorageService.setTabState(tabId, newState).catch(() => { });
applyIconForState(tabId, newState);
}
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);
applyIconForState(tabId, newState);
}
}
export default defineBackground({ export default defineBackground({
main() { main() {
browser.runtime.onMessage.addListener( browser.runtime.onStartup.addListener(() => {
(request: any, _sender, sendResponse) => { StorageService.cleanExpiredGeoCache().catch(console.error);
if (request.type === 'FETCH_SERVER_INFO') { });
;(async () => {
try {
const cleanHostname =
request.hostname.startsWith('[') &&
request.hostname.endsWith(']')
? request.hostname.slice(1, -1)
: request.hostname
const apiData = await getIPInfo(cleanHostname) browser.runtime.onInstalled.addListener(() => {
if (!apiData || !apiData.ip) { StorageService.cleanExpiredGeoCache().catch(console.error);
sendResponse({ error: 'DNS resolution failed', data: null }) });
return
}
if (apiData.bogon === true) {
await updateIcon(null)
sendResponse({
error: null,
data: {
origin: '',
ip: cleanHostname,
hostname: '',
country: '',
city: '',
org: '',
isLocal: true,
isBrowserResource: false,
},
})
return
}
const parsed = psl.parse(cleanHostname) browser.alarms.create('cleanup-geo-cache', { periodInMinutes: 1440 });
const origin = 'domain' in parsed ? parsed.domain : null browser.alarms.onAlarm.addListener((alarm) => {
const country = apiData.country || null if (alarm.name === 'cleanup-geo-cache') {
const asn = apiData.org?.split(' ')[0] StorageService.cleanExpiredGeoCache().catch(console.error);
let iconCode = country
if (!iconCode && asn === 'AS13335') {
iconCode = 'cloudflare'
}
await updateIcon(iconCode)
sendResponse({
error: null,
data: {
origin,
ip: apiData.ip,
hostname: apiData.hostname || 'N/A',
country: apiData.country || null,
city: apiData.city || null,
org: apiData.org,
isLocal: false,
isBrowserResource: false,
},
})
} catch (error) {
await updateIcon(null)
sendResponse({
error: error instanceof Error ? error.message : 'Unknown error',
data: null,
})
}
})()
return true
}
sendResponse({ error: 'Unknown request type', data: null })
return true
} }
) });
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);
});
browser.webNavigation.onHistoryStateUpdated.addListener((details) => {
if (details.frameId !== 0) return;
initTab(details.tabId, details.url, true);
});
browser.webNavigation.onCommitted.addListener((details) => {
if (details.frameId !== 0) return;
const state = tabStates.get(details.tabId);
if (state) {
applyIconForState(details.tabId, state);
}
});
browser.webRequest.onResponseStarted.addListener(
(details) => {
if (details.tabId === -1 || details.type !== 'main_frame') return;
if (details.ip) {
processIp(details.tabId, details.url, details.ip);
}
},
{ urls: ['<all_urls>'] }
);
browser.webNavigation.onCompleted.addListener(async (details) => {
if (details.frameId !== 0) return;
const state = tabStates.get(details.tabId);
if (state) {
applyIconForState(details.tabId, state);
}
if (state && state.status === 'loading' && !state.data) {
let hostname = '';
try {
hostname = new URL(details.url).hostname;
} catch {
return;
}
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);
}
}
});
browser.webRequest.onErrorOccurred.addListener(
async (details) => {
if (details.type !== 'main_frame') 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>'] }
);
browser.tabs.onRemoved.addListener((tabId) => {
tabStates.delete(tabId);
StorageService.removeTabState(tabId);
});
browser.tabs.onActivated.addListener(async (activeInfo) => {
const tab = await browser.tabs.get(activeInfo.tabId);
if (tab.url) {
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);
}
}
});
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)
}
}
}
}
}
});
browser.runtime.onMessage.addListener((message) => {
if (message.type === 'INIT_TAB' && message.tabId && message.url) {
initTab(message.tabId, message.url, true);
}
});
}, },
}) });
+9 -8
View File
@@ -1,20 +1,21 @@
import ServerInfo from '@/components/ServerInfo'; import ServerInfo from '@/components/ServerInfo';
import Error from '@/components/Error'; import Error from '@/components/Error';
import { useHostInfo } from '@/hooks/useHostInfo';
export default function Popup() { export default function Popup() {
const { data, error } = useTabData(); const { info, loading } = useHostInfo();
if (error) { if (loading) {
return ( return (
<Error error={error} /> <div className="w-80 h-64 bg-white dark:bg-gray-950 flex flex-col items-center justify-center font-sans">
<div className="w-6 h-6 border-2 border-gray-200 dark:border-gray-700 border-t-blue-600 rounded-full animate-spin"></div>
</div>
); );
} }
if (!data) { if (!info) {
return ( return <Error error="No active page found" />;
<Error error="No data found" />
);
} }
return <ServerInfo data={data} />; return <ServerInfo state={info} />;
} }
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Host Info</title> <title>Host Info</title>
<meta name="manifest.type" content="browser_action" /> <meta name="manifest.type" content="action" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+62
View File
@@ -0,0 +1,62 @@
export function useHostInfo() {
const [info, setInfo] = useState<TabState | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true;
const fetchInfo = async () => {
try {
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
if (!tab?.id) {
if (isMounted) setLoading(false);
return;
}
const data = await StorageService.getTabState(tab.id);
if (data) {
if (isMounted) {
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 });
} else {
if (isMounted) setLoading(false);
}
}
} catch (e) {
if (isMounted) setLoading(false);
}
};
fetchInfo();
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);
return () => {
isMounted = false;
browser.storage.onChanged.removeListener(listener);
};
}, []);
return { info, loading };
}
-75
View File
@@ -1,75 +0,0 @@
export function useTabData() {
const [data, setData] = useState<ServerData | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchData = async () => {
try {
const [tab] = await browser.tabs.query({
active: true,
currentWindow: true,
})
if (!tab?.url) throw new Error('No active tab found')
const url = new URL(tab.url)
const hostname = url.hostname
if (['chrome:', 'about:', 'file:'].includes(url.protocol)) {
return setData({
origin: '',
ip: '',
hostname: url.href,
country: '',
city: '',
org: '',
isLocal: false,
isBrowserResource: true,
})
}
const response = await browser.runtime.sendMessage<
FetchServerInfoRequest,
FetchServerInfoResponse
>({
type: 'FETCH_SERVER_INFO',
hostname: hostname,
})
if (!response) {
throw new Error('No response from background script')
}
if (response.error) {
return setData({
origin: '',
ip: '',
hostname: hostname,
country: '',
city: '',
org: '',
isLocal: true,
isBrowserResource: false,
})
}
if (!response.data?.ip) {
throw new Error('Invalid server data received')
}
setData(response.data)
setError(null)
} catch (err) {
setError(err instanceof Error ? err.message : 'No data found')
setData(null)
} finally {
setLoading(false)
}
}
fetchData()
}, [])
return { data, loading, error }
}
+14 -19
View File
@@ -2,36 +2,31 @@
"name": "hostinfo", "name": "hostinfo",
"description": "Receive information of a domain directly in the browser when browsing a website", "description": "Receive information of a domain directly in the browser when browsing a website",
"private": true, "private": true,
"version": "1.5", "version": "2.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "wxt", "dev": "wxt",
"dev:firefox": "wxt -b firefox", "dev:firefox": "wxt -b firefox",
"build": "wxt build", "build": "wxt build",
"build:firefox": "wxt build -b firefox", "build:firefox": "wxt build -b firefox --mv3",
"zip": "wxt zip", "zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox", "zip:firefox": "wxt zip -b firefox --mv3",
"compile": "tsc --noEmit", "compile": "tsc --noEmit",
"postinstall": "wxt prepare" "postinstall": "wxt prepare"
}, },
"dependencies": { "dependencies": {
"@heroicons/react": "^2.2.0", "@heroicons/react": "^2.2.0",
"@tailwindcss/vite": "^4.0.15", "@tailwindcss/vite": "^4.2.1",
"@types/psl": "^1.1.3", "react": "^19.2.4",
"@types/webextension-polyfill": "^0.12.3", "react-dom": "^19.2.4",
"react": "^19.0.0", "tailwindcss": "^4.2.1"
"react-dom": "^19.0.0",
"tailwindcss": "^4.0.15"
}, },
"devDependencies": { "devDependencies": {
"@types/chrome": "^0.0.310", "@types/chrome": "^0.1.37",
"@types/react": "^19.0.12", "@types/react": "^19.2.14",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.2.3",
"@wxt-dev/module-react": "^1.1.3", "@wxt-dev/module-react": "^1.2.1",
"typescript": "^5.8.2", "typescript": "^5.9.3",
"wxt": "^0.19.29" "wxt": "^0.20.18"
}, }
"trustedDependencies": [
"spawn-sync"
]
} }
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 668 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 488 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 654 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 678 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 512 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 743 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 878 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 878 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 444 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 694 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 794 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 510 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 470 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 B

Some files were not shown because too many files have changed in this diff Show More