40 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
x f2cc9a8c87 bump package versions 2025-03-24 20:05:11 +01:00
x bf5c5575f5 refactor icon handling to use webextension-polyfill for browser compatibility 2025-03-24 20:01:31 +01:00
x 6f0125896a refactor DNS resolution and IP handling; remove unused utility 2025-03-24 19:47:26 +01:00
x 9253e53ca1 chrome approved 2025-03-24 01:18:35 +01:00
x d5a4fcbe5a bump version to 1.5 2025-03-19 22:08:35 +01:00
x c3be23d369 handle local domains 2025-03-19 22:08:30 +01:00
x 90bb276622 fix permissions (remove webrequest) 2025-03-19 22:08:20 +01:00
x 806072fbf1 bump version to 1.4 2025-03-18 15:56:41 +01:00
x baa277fbd9 fix manifest (dns server) 2025-03-18 15:56:20 +01:00
x 3e9fe9a199 bump version to 1.3 2025-03-18 12:55:38 +01:00
x a540f1eb0a add cloudflare 2025-03-18 12:55:32 +01:00
x 940b8bbec4 fix manifest 2025-03-18 12:55:17 +01:00
x 3419544ba4 firefox approved 2025-03-17 19:49:27 +01:00
x 87b4c93268 Create PRIVACY.md 2025-03-16 11:32:29 +01:00
x feaaa65960 add showcase 2025-03-15 22:05:41 +01:00
539 changed files with 1395 additions and 1025 deletions
+36
View File
@@ -0,0 +1,36 @@
# Privacy Policy
**Last Updated:** 2025-03-16
Thank you for using HostInfo (the "Extension"). This Privacy Policy explains how we handle your information when you use our Extension. Please read this policy carefully to understand our practices regarding your data.
## 1. **Information We Do Not Collect**
HostInfo does not collect, store, or transmit any personal or sensitive information from its users. This includes, but is not limited to:
- Personal identification information (e.g., name, email address, phone number).
- Browsing history or activity.
- IP addresses or location data (except as described below for GeoIP lookups).
- Any other data that could be used to identify you.
## 2. **Third-Party Services**
The Extension uses the following third-party services to provide functionality:
- **Cloudflare DNS (`cloudflare-dns.com`)**: The Extension queries hostnames using Cloudflare's DNS service to resolve website IP addresses. This is done to provide accurate and fast DNS resolution. Cloudflare's privacy policy can be found [here](https://www.cloudflare.com/privacypolicy/).
- **GeoIP Lookups (`ip.albert.lol`)**: The Extension uses `ip.albert.lol` to perform GeoIP lookups. This service may receive your IP address to determine your approximate geographic location (e.g., country or region). No other personal information is shared with this service.
Neither of these services is used to collect, store, or track your personal information. The data sent to these services is used solely for the purpose of providing the Extension's functionality.
## 3. **How We Use Information**
Since we do not collect any personal information, there is no data to use, share, or sell. The Extension operates locally on your device and only communicates with the aforementioned third-party services for DNS resolution and GeoIP lookups.
## 4. **Changes to This Privacy Policy**
We may update this Privacy Policy from time to time. If we make any changes, we will update the "Last Updated" date at the top of this policy. We encourage you to review this Privacy Policy periodically to stay informed about how we are protecting your information.
## 5. **Contact Us**
If you have any questions or concerns about this Privacy Policy, please feel free to contact us at `contact@albert.lol`.
+5
View File
@@ -2,6 +2,11 @@
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 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://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>
--- ---
## ✨ Features ## ✨ Features
+373 -601
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 -86
View File
@@ -1,97 +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>
<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>
</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>
);
} }
+294 -84
View File
@@ -1,107 +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 {
const dnsResponse = await fetch( return new URL(url).hostname;
`https://cloudflare-dns.com/dns-query?name=${hostname}&type=A`, } catch {
{ return url;
headers: { Accept: 'application/dns-json' }, }
}
function applyIconForState(tabId: number, state: TabState) {
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;
if (state.data.asn === 'AS13335') code = 'cloudflare';
IconService.updateIcon(tabId, code, state.data.isLocal);
} else {
IconService.updateIcon(tabId, null, false);
}
}
async function initTab(tabId: number, url: string, resolveDns = false) {
if (!url) return;
const isWebProtocol = url.startsWith('http://') || url.startsWith('https://');
const domain = getDomain(url);
let currentState = tabStates.get(tabId);
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 (!dnsResponse.ok) { if (resolveDns) {
console.error(`DNS query failed: ${dnsResponse.status}`) await performDnsFallback();
return null } else {
setTimeout(performDnsFallback, 1500);
} }
const dnsData = await dnsResponse.json()
return (
dnsData.Answer?.find((entry: DNSEntry) => entry.type === 1)?.data || null
)
} catch (error) {
console.error('Failed to fetch DNS data:', error)
return null
} }
} }
async function handleTabUpdate(url: string) { async function processIp(tabId: number, url: string, ip: string) {
if (url === currentTabUrl) return let current = tabStates.get(tabId);
currentTabUrl = url if (!current) {
current = await StorageService.getTabState(tabId) || undefined;
try {
const hostname = new URL(url).hostname
const ip = await resolveARecord(hostname)
if (!ip) {
await updateIcon(null)
return
}
const apiResponse = await fetch(`https://ip.albert.lol/${ip}`)
const apiData = await apiResponse.json()
await updateIcon(apiData.country || null)
} catch (error) {
console.error('Failed to handle tab update:', error)
await updateIcon(null)
} }
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);
} }
browser.tabs.onActivated.addListener(async activeInfo => { async function updateState(tabId: number, updates: Partial<TabState>, expectedUrl?: string) {
const tab = await browser.tabs.get(activeInfo.tabId) let current = tabStates.get(tabId);
if (tab.url) await handleTabUpdate(tab.url) if (!current) {
}) current = await StorageService.getTabState(tabId) || undefined;
}
browser.tabs.onUpdated.addListener(async (_tabId, changeInfo) => { if (current) {
if (changeInfo.url) await handleTabUpdate(changeInfo.url) 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((request: any, _sender, sendResponse) => { browser.runtime.onStartup.addListener(() => {
if (request.type === 'FETCH_SERVER_INFO') { StorageService.cleanExpiredGeoCache().catch(console.error);
;(async () => { });
try {
const ip = await resolveARecord(request.hostname)
if (!ip) {
sendResponse({ error: 'DNS resolution failed', data: null })
return
}
const apiResponse = await fetch(`https://ip.albert.lol/${ip}`) browser.runtime.onInstalled.addListener(() => {
const apiData = await apiResponse.json() StorageService.cleanExpiredGeoCache().catch(console.error);
});
const parsed = psl.parse(request.hostname) browser.alarms.create('cleanup-geo-cache', { periodInMinutes: 1440 });
const origin = 'domain' in parsed ? parsed.domain : null browser.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'cleanup-geo-cache') {
StorageService.cleanExpiredGeoCache().catch(console.error);
}
});
await updateIcon(apiData.country) browser.tabs.onReplaced.addListener((addedTabId, removedTabId) => {
tabStates.delete(removedTabId);
StorageService.removeTabState(removedTabId);
sendResponse({ browser.tabs.get(addedTabId).then((tab) => {
error: null, if (tab.url) {
data: { initTab(tab.id!, tab.url, true)
origin, }
ip: apiData.ip, }).catch(() => { })
hostname: apiData.hostname || 'N/A', })
country: apiData.country || null,
city: apiData.city || null, browser.webNavigation.onBeforeNavigate.addListener((details) => {
org: apiData.org, if (details.frameId !== 0) return;
}, initTab(details.tabId, details.url, false);
}) });
} catch (error) {
await updateIcon(null) browser.webNavigation.onHistoryStateUpdated.addListener((details) => {
sendResponse({ if (details.frameId !== 0) return;
error: error instanceof Error ? error.message : 'Unknown error', initTab(details.tabId, details.url, true);
data: null, });
})
} browser.webNavigation.onCommitted.addListener((details) => {
})() if (details.frameId !== 0) return;
return true 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);
} }
sendResponse({ error: 'Unknown request type', data: null }) if (state && state.status === 'loading' && !state.data) {
return true 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 };
}
-82
View File
@@ -1,82 +0,0 @@
import { useState, useEffect } from 'react';
import browser from 'webextension-polyfill';
import { isPrivateIP } from '@/utils';
import { FetchServerInfoRequest, FetchServerInfoResponse, ServerData } from '@/utils/model';
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 isInternal = isPrivateIP(hostname)
if (isInternal) {
return setData({
origin: '',
ip: hostname,
hostname: url.href,
country: '',
city: '',
org: '',
isLocal: true,
isBrowserResource: false,
})
}
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) {
throw new Error(response.error)
}
if (!response.data?.ip) {
throw new Error('Invalid server data received')
}
setData(response.data)
setError(null)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch data')
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.2", "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.14", "@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.14"
}, },
"devDependencies": { "devDependencies": {
"@types/chrome": "^0.0.309", "@types/chrome": "^0.1.37",
"@types/react": "^19.0.10", "@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

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