23 Commits

Author SHA1 Message Date
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 1275 additions and 1020 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.
<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
+377 -593
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>
);
};
+23
View File
@@ -0,0 +1,23 @@
import { CheckIcon, ClipboardDocumentIcon } from '@heroicons/react/24/outline';
export const CopyButton = ({ text }: { text: string }) => {
const [copied, setCopied] = useState(false);
const handleCopy = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
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 }) {
return (
<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">
Error
</h2>
</div>
<div className="flex items-center space-x-3">
<div>
<p className="text-sm text-gray-300">{error}</p>
</div>
<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="p-3 bg-red-50 dark:bg-red-900/20 rounded-full mb-4">
<ExclamationTriangleIcon className="w-6 h-6 text-red-600 dark:text-red-400" />
</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>
);
}
+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={domain}
canCopy
iconColor="text-indigo-500"
/>
<InfoRow
icon={MapPinIcon}
label="Location"
value={data.countryName || 'Unknown Location'}
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 { codes } from '@/utils/codes';
import type { TabState } from '@/utils/types';
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
? codes[data.country.toLowerCase()] || "N/A"
: "N/A";
if (state.status === 'error') {
return <Error error={state.errorMessage || 'Unknown Error'} />;
}
if (data.isBrowserResource) {
if (state.status === 'success' && !state.data) {
return <BrowserResourceView url={state.url} />;
}
if (state.status === 'loading' || !state.data) {
return (
<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">
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 className="w-80 h-64 flex flex-col items-center justify-center space-y-3 bg-white dark:bg-gray-950">
<div className="w-6 h-6 border-2 border-gray-200 dark:border-gray-700 border-t-blue-600 rounded-full animate-spin"></div>
<span className="text-xs text-gray-400 font-medium">Analyzing Network...</span>
</div>
);
}
if (data.isLocal) {
return (
<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>
);
if (state.data.isLocal || state.data.isBogon) {
return <LocalNetworkView data={state.data} domain={state.domain} />;
}
return (
<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>
);
return <PublicNetworkView data={state.data} domain={state.domain} />;
}
+232 -86
View File
@@ -1,107 +1,253 @@
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 SYSTEM_PROTOCOLS = [
'chrome:', 'about:', 'edge:', 'moz-extension:',
'chrome-extension:', 'file:', 'view-source:', 'data:', 'devtools:'
];
async function resolveARecord(hostname: string): Promise<string | null> {
const tabStates = new Map<number, TabState>();
function getDomain(url: string) {
try {
const dnsResponse = await fetch(
`https://cloudflare-dns.com/dns-query?name=${hostname}&type=A`,
{
headers: { Accept: 'application/dns-json' },
}
)
if (!dnsResponse.ok) {
console.error(`DNS query failed: ${dnsResponse.status}`)
return null
}
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
return new URL(url).hostname;
} catch {
return url;
}
}
async function handleTabUpdate(url: string) {
if (url === currentTabUrl) return
currentTabUrl = url
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)
function applyIconForState(tabId: number, state: TabState) {
const isSystem = SYSTEM_PROTOCOLS.some(p => state.url.startsWith(p));
if (isSystem) {
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);
}
}
browser.tabs.onActivated.addListener(async activeInfo => {
const tab = await browser.tabs.get(activeInfo.tabId)
if (tab.url) await handleTabUpdate(tab.url)
})
async function initTab(tabId: number, url: string, resolveDns = false) {
if (!url) return;
const isSystem = SYSTEM_PROTOCOLS.some(p => url.startsWith(p));
const domain = getDomain(url);
browser.tabs.onUpdated.addListener(async (_tabId, changeInfo) => {
if (changeInfo.url) await handleTabUpdate(changeInfo.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: isSystem || hasExistingData ? 'success' : 'loading',
data: hasExistingData ? currentState!.data : null,
errorMessage: null,
lastUpdated: Date.now()
};
tabStates.set(tabId, newState);
StorageService.setTabState(tabId, newState).catch(() => { });
applyIconForState(tabId, newState);
if (!isSystem && !hasExistingData && resolveDns) {
const ip = await DnsService.resolve(domain);
if (ip) {
await processIp(tabId, url, ip);
} else {
await updateState(tabId, {
status: 'error',
errorMessage: 'Could not resolve host'
});
}
}
}
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);
applyIconForState(tabId, newState);
}
async function updateState(tabId: number, updates: Partial<TabState>) {
let current = tabStates.get(tabId);
if (!current) {
current = await StorageService.getTabState(tabId) || undefined;
}
if (current) {
const newState = { ...current, ...updates };
tabStates.set(tabId, newState);
await StorageService.setTabState(tabId, newState);
applyIconForState(tabId, newState);
}
}
export default defineBackground({
main() {
browser.runtime.onMessage.addListener((request: any, _sender, sendResponse) => {
if (request.type === 'FETCH_SERVER_INFO') {
;(async () => {
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 {
const ip = await resolveARecord(request.hostname)
if (!ip) {
sendResponse({ error: 'DNS resolution failed', data: null })
return
hostname = new URL(details.url).hostname;
} catch {
return;
}
const ip = await DnsService.resolve(hostname);
const apiResponse = await fetch(`https://ip.albert.lol/${ip}`)
const apiData = await apiResponse.json()
if (ip) {
await processIp(details.tabId, details.url, ip);
} else {
await updateState(details.tabId, {
status: 'error',
errorMessage: 'Could not resolve host'
});
}
}
});
const parsed = psl.parse(request.hostname)
const origin = 'domain' in parsed ? parsed.domain : null
browser.webRequest.onErrorOccurred.addListener(
async (details) => {
if (details.type !== 'main_frame') return;
if (details.error === 'net::ERR_ABORTED') return;
await updateIcon(apiData.country)
sendResponse({
error: null,
data: {
origin,
ip: apiData.ip,
hostname: apiData.hostname || 'N/A',
country: apiData.country || null,
city: apiData.city || null,
org: apiData.org,
await updateState(details.tabId, {
status: 'error',
errorMessage: details.error
});
},
})
} catch (error) {
await updateIcon(null)
sendResponse({
error: error instanceof Error ? error.message : 'Unknown error',
data: null,
})
}
})()
return true
}
{ urls: ['<all_urls>'] }
);
sendResponse({ error: 'Unknown request type', data: null })
return true
})
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);
} else {
initTab(tab.id!, tab.url, true);
}
}
});
browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status && tab.url) {
const state = tabStates.get(tabId);
if (state) {
applyIconForState(tabId, state);
}
}
});
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 Error from '@/components/Error';
import { useHostInfo } from '@/hooks/useHostInfo';
export default function Popup() {
const { data, error } = useTabData();
const { info, loading } = useHostInfo();
if (error) {
if (loading) {
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) {
return (
<Error error="No data found" />
);
if (!info) {
return <Error error="No active page found" />;
}
return <ServerInfo data={data} />;
return <ServerInfo state={info} />;
}
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Host Info</title>
<meta name="manifest.type" content="browser_action" />
<meta name="manifest.type" content="action" />
</head>
<body>
<div id="root"></div>
+48
View File
@@ -0,0 +1,48 @@
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) return;
const data = await StorageService.getTabState(tab.id);
if (data) {
if (isMounted) {
setInfo(data);
setLoading(false);
}
} 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') {
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",
"description": "Receive information of a domain directly in the browser when browsing a website",
"private": true,
"version": "1.2",
"version": "2.0.0",
"type": "module",
"scripts": {
"dev": "wxt",
"dev:firefox": "wxt -b firefox",
"build": "wxt build",
"build:firefox": "wxt build -b firefox",
"build:firefox": "wxt build -b firefox --mv3",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"zip:firefox": "wxt zip -b firefox --mv3",
"compile": "tsc --noEmit",
"postinstall": "wxt prepare"
},
"dependencies": {
"@heroicons/react": "^2.2.0",
"@tailwindcss/vite": "^4.0.14",
"@types/psl": "^1.1.3",
"@types/webextension-polyfill": "^0.12.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^4.0.14"
"@tailwindcss/vite": "^4.2.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"tailwindcss": "^4.2.1"
},
"devDependencies": {
"@types/chrome": "^0.0.309",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@wxt-dev/module-react": "^1.1.3",
"typescript": "^5.8.2",
"wxt": "^0.19.29"
},
"trustedDependencies": [
"spawn-sync"
]
"@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"
}
}
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