Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
da23868817
|
|||
|
9292a4a6e2
|
|||
|
2973e038d6
|
|||
|
991cd10f40
|
|||
|
6bf00dc6e0
|
|||
|
a4ae08870d
|
|||
|
f2cc9a8c87
|
|||
|
bf5c5575f5
|
|||
|
6f0125896a
|
|||
| 9253e53ca1 |
@@ -5,6 +5,7 @@ A browser extension built with [WXT.dev](https://wxt.dev) and React that lets yo
|
|||||||
<img src="https://github.com/user-attachments/assets/83a6316c-54b8-41a8-8d43-c794a5f62696" alt="Showcase" width="200"/>
|
<img src="https://github.com/user-attachments/assets/83a6316c-54b8-41a8-8d43-c794a5f62696" alt="Showcase" width="200"/>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { CpuChipIcon, GlobeAltIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { Header } from './Header';
|
||||||
|
import { InfoRow } from './Info';
|
||||||
|
import type { HostInfo } from '@/utils/types';
|
||||||
|
|
||||||
|
export const BrowserResourceView = ({ data }: { data: HostInfo }) => {
|
||||||
|
return (
|
||||||
|
<div className="w-80 bg-white dark:bg-gray-950 font-sans">
|
||||||
|
<Header
|
||||||
|
title="System Resource"
|
||||||
|
flagCode="unknown"
|
||||||
|
/>
|
||||||
|
<div className="p-5">
|
||||||
|
<InfoRow
|
||||||
|
icon={CpuChipIcon}
|
||||||
|
label="Type"
|
||||||
|
value="Local Browser Page"
|
||||||
|
iconColor="text-orange-500"
|
||||||
|
/>
|
||||||
|
<InfoRow
|
||||||
|
icon={GlobeAltIcon}
|
||||||
|
label="URL"
|
||||||
|
value={data.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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { browser } from 'wxt/browser';
|
||||||
|
|
||||||
|
export const Header = ({ title, flagCode }: { title: string, flagCode?: string | null }) => {
|
||||||
|
const getFlagUrl = (code?: string | null) => {
|
||||||
|
if (!code) return '';
|
||||||
|
try {
|
||||||
|
const path = `/${code.toLowerCase()}.webp`;
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { CpuChipIcon, ServerIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { Header } from './Header';
|
||||||
|
import { InfoRow } from './Info';
|
||||||
|
import type { HostInfo } from '@/utils/types';
|
||||||
|
|
||||||
|
export const LocalNetworkView = ({ data }: { data: HostInfo }) => {
|
||||||
|
return (
|
||||||
|
<div className="w-80 bg-white dark:bg-gray-950 font-sans">
|
||||||
|
<Header
|
||||||
|
title={data.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.network?.ip || null}
|
||||||
|
canCopy
|
||||||
|
iconColor="text-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
ServerIcon,
|
||||||
|
MapPinIcon,
|
||||||
|
GlobeAltIcon,
|
||||||
|
BuildingOfficeIcon
|
||||||
|
} from '@heroicons/react/24/outline';
|
||||||
|
import { Header } from './Header';
|
||||||
|
import { InfoRow } from './Info';
|
||||||
|
import type { HostInfo } from '@/utils/types';
|
||||||
|
|
||||||
|
export const PublicNetworkView = ({ data }: { data: HostInfo }) => {
|
||||||
|
const { network, location, domain } = data;
|
||||||
|
|
||||||
|
if (!network) return null;
|
||||||
|
|
||||||
|
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={location?.countryCode || 'unknown'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="p-5 space-y-0.5">
|
||||||
|
<InfoRow
|
||||||
|
icon={ServerIcon}
|
||||||
|
label="IP Address"
|
||||||
|
value={network.ip}
|
||||||
|
href={`https://ip.albert.lol/${network.ip}`}
|
||||||
|
canCopy
|
||||||
|
iconColor="text-blue-500"
|
||||||
|
/>
|
||||||
|
<InfoRow
|
||||||
|
icon={GlobeAltIcon}
|
||||||
|
label="Hostname"
|
||||||
|
value={network.hostname}
|
||||||
|
canCopy
|
||||||
|
iconColor="text-indigo-500"
|
||||||
|
/>
|
||||||
|
<InfoRow
|
||||||
|
icon={MapPinIcon}
|
||||||
|
label="Location"
|
||||||
|
value={location?.countryName || 'Unknown Location'}
|
||||||
|
iconColor="text-emerald-500"
|
||||||
|
/>
|
||||||
|
<InfoRow
|
||||||
|
icon={BuildingOfficeIcon}
|
||||||
|
label="Organization / ASN"
|
||||||
|
value={network.org}
|
||||||
|
href={network.asn ? `https://bgp.he.net/${network.asn}` : undefined}
|
||||||
|
iconColor="text-violet-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-5 pb-5 pt-2">
|
||||||
|
<a
|
||||||
|
href={`https://platform.censys.io/search?q=${network.hostname || domain}`}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,110 +1,27 @@
|
|||||||
import { LinkIcon, ServerIcon, IdentificationIcon, MapPinIcon } from '@heroicons/react/24/outline';
|
import type { HostInfo } 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({ data }: { data: HostInfo }) {
|
||||||
|
const { network, isBrowserResource } = data;
|
||||||
|
|
||||||
const countryName = data.country
|
// Browser Resource View
|
||||||
? codes[data.country.toLowerCase()] || "N/A"
|
if (isBrowserResource) {
|
||||||
: "N/A";
|
return <BrowserResourceView data={data} />;
|
||||||
|
|
||||||
if (data.isBrowserResource) {
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.isLocal) {
|
// Fallback if network data is missing
|
||||||
return (
|
if (!network) {
|
||||||
<div className="min-w-[300px] bg-gray-900 shadow-2xl p-6 text-white font-sans">
|
return <Error error="Host information unavailable." />;
|
||||||
<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 (
|
// Local Network View
|
||||||
<div className="min-w-[300px] bg-gray-900 shadow-2xl p-6 text-white font-sans">
|
if (network.isLocal) {
|
||||||
<div className="flex items-center justify-between mb-6">
|
return <LocalNetworkView data={data} />;
|
||||||
<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">
|
// Public Internet View
|
||||||
<ServerIcon className="w-6 h-6 text-yellow-400 flex-shrink-0" />
|
return <PublicNetworkView data={data} />;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +1,51 @@
|
|||||||
import psl from 'psl'
|
import { browser } from 'wxt/browser';
|
||||||
|
import { Tab } from '@/services/tab';
|
||||||
let currentTabUrl: string | null = null
|
import { StorageService } from '@/utils/storage';
|
||||||
|
|
||||||
async function resolveARecord(hostname: string): Promise<string | null> {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
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 => {
|
|
||||||
const tab = await browser.tabs.get(activeInfo.tabId)
|
|
||||||
if (tab.url) await handleTabUpdate(tab.url)
|
|
||||||
})
|
|
||||||
|
|
||||||
browser.tabs.onUpdated.addListener(async (_tabId, changeInfo) => {
|
|
||||||
if (changeInfo.url) await handleTabUpdate(changeInfo.url)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default defineBackground({
|
export default defineBackground({
|
||||||
main() {
|
main() {
|
||||||
browser.runtime.onMessage.addListener(
|
// Listen for Network Responses
|
||||||
(request: any, _sender, sendResponse) => {
|
browser.webRequest.onResponseStarted.addListener(
|
||||||
if (request.type === 'FETCH_SERVER_INFO') {
|
async (details) => {
|
||||||
;(async () => {
|
if (details.tabId === -1 || details.type !== 'main_frame' || !details.ip) return;
|
||||||
try {
|
await Tab.process(details.tabId, details.url, details.ip);
|
||||||
const ip = await resolveARecord(request.hostname)
|
},
|
||||||
if (!ip) {
|
{ urls: ['<all_urls>'] }
|
||||||
sendResponse({ error: 'DNS resolution failed', data: null })
|
);
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiResponse = await fetch(`https://ip.albert.lol/${ip}`)
|
// Listen for Network Errors (DNS, Connection Refused, etc.)
|
||||||
const apiData = await apiResponse.json()
|
browser.webRequest.onErrorOccurred.addListener(
|
||||||
|
async (details) => {
|
||||||
|
if (details.tabId === -1 || details.type !== 'main_frame') return;
|
||||||
|
await Tab.handleError(details.tabId, details.url, details.error);
|
||||||
|
},
|
||||||
|
{ urls: ['<all_urls>'] }
|
||||||
|
);
|
||||||
|
|
||||||
const parsed = psl.parse(request.hostname)
|
// Listen for Navigation
|
||||||
const origin = 'domain' in parsed ? parsed.domain : null
|
browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
|
||||||
|
if (changeInfo.status === 'complete' && tab.url) {
|
||||||
|
const urlObj = new URL(tab.url);
|
||||||
|
const isSystemPage = ['chrome:', 'about:', 'edge:', 'moz-extension:', 'chrome-extension:', 'file:'].includes(urlObj.protocol);
|
||||||
|
|
||||||
const country = apiData.country || null
|
if (isSystemPage) {
|
||||||
const asn = apiData.org?.split(' ')[0]
|
await Tab.processSystemPage(tabId);
|
||||||
let iconCode = country
|
} else if (tab.url.startsWith('http')) {
|
||||||
if (!iconCode && asn === 'AS13335') {
|
// We might not have the IP yet if it was cached, so we trigger a process
|
||||||
iconCode = 'cloudflare'
|
// If IP is missing, Tab waits or we can force a HEAD request here
|
||||||
}
|
await Tab.process(tabId, tab.url);
|
||||||
await updateIcon(iconCode)
|
|
||||||
|
|
||||||
sendResponse({
|
// Force connection to ensure webRequest fires if cached
|
||||||
error: null,
|
try {
|
||||||
data: {
|
await fetch(tab.url, { method: 'HEAD', cache: 'no-store', mode: 'no-cors' });
|
||||||
origin,
|
} catch { /* ignore */ }
|
||||||
ip: apiData.ip,
|
|
||||||
hostname: apiData.hostname || 'N/A',
|
|
||||||
country: apiData.country || null,
|
|
||||||
city: apiData.city || null,
|
|
||||||
org: apiData.org,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} 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
|
|
||||||
}
|
}
|
||||||
)
|
});
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
browser.tabs.onRemoved.addListener(async (tabId) => {
|
||||||
|
await StorageService.remove(tabId);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
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-75 bg-white dark:bg-gray-950 flex flex-col items-center justify-center space-y-4 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>
|
||||||
|
<p className="text-gray-400 text-xs font-medium">Loading host info...</p>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data) {
|
if (!info) {
|
||||||
return (
|
return <Error error="No active page found" />;
|
||||||
<Error error="No data found" />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ServerInfo data={data} />;
|
if (info.error) {
|
||||||
|
return <Error error={info.error} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ServerInfo data={info} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { browser } from 'wxt/browser';
|
||||||
|
import { StorageService } from '@/utils/storage';
|
||||||
|
import { IconService } from '@/services/icon';
|
||||||
|
import type { HostInfo } from '@/utils/types';
|
||||||
|
|
||||||
|
export function useHostInfo() {
|
||||||
|
const [info, setInfo] = useState<HostInfo | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let unwatch: (() => void) | undefined;
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
// Get Current Tab
|
||||||
|
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
|
||||||
|
if (!tab?.id || !tab?.url) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle System/Browser Pages immediately
|
||||||
|
const urlObj = new URL(tab.url);
|
||||||
|
|
||||||
|
const systemProtocols = [
|
||||||
|
'chrome:',
|
||||||
|
'about:',
|
||||||
|
'edge:',
|
||||||
|
'moz-extension:',
|
||||||
|
'chrome-extension:',
|
||||||
|
'edge-extension:',
|
||||||
|
'extension:',
|
||||||
|
'file:',
|
||||||
|
'view-source:',
|
||||||
|
'resource:',
|
||||||
|
'blob:',
|
||||||
|
'data:'
|
||||||
|
];
|
||||||
|
|
||||||
|
const isSystemPage = systemProtocols.includes(urlObj.protocol);
|
||||||
|
|
||||||
|
if (isSystemPage) {
|
||||||
|
await IconService.update(tab.id, null, true);
|
||||||
|
|
||||||
|
setInfo({
|
||||||
|
url: tab.url,
|
||||||
|
domain: 'System Resource',
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
network: null,
|
||||||
|
location: null,
|
||||||
|
isBrowserResource: true
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Network Pages via Storage
|
||||||
|
const key = StorageService.getKey(tab.id);
|
||||||
|
|
||||||
|
// Initial Load
|
||||||
|
const current = await StorageService.get(tab.id);
|
||||||
|
setInfo(current);
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
// Watch for changes
|
||||||
|
unwatch = storage.watch<HostInfo>(key, (newValue) => {
|
||||||
|
setInfo(newValue);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (unwatch) unwatch();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { info, loading };
|
||||||
|
}
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react'
|
|
||||||
import { isPrivateIP } from '@/utils'
|
|
||||||
import {
|
|
||||||
FetchServerInfoRequest,
|
|
||||||
FetchServerInfoResponse,
|
|
||||||
ServerData,
|
|
||||||
} from '@/utils/model'
|
|
||||||
import browser from 'webextension-polyfill'
|
|
||||||
|
|
||||||
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: '',
|
|
||||||
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) {
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
@@ -2,36 +2,33 @@
|
|||||||
"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.0.0",
|
||||||
"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.1.18",
|
||||||
"@types/psl": "^1.1.3",
|
"clsx": "^2.1.1",
|
||||||
"@types/webextension-polyfill": "^0.12.3",
|
"react": "^19.2.4",
|
||||||
"react": "^19.0.0",
|
"react-dom": "^19.2.4",
|
||||||
"react-dom": "^19.0.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.0.14"
|
"tailwindcss": "^4.1.18"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chrome": "^0.0.309",
|
"@types/chrome": "^0.1.36",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.2.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@wxt-dev/module-react": "^1.1.3",
|
"@wxt-dev/module-react": "^1.1.5",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.9.3",
|
||||||
"wxt": "^0.19.29"
|
"wxt": "^0.20.13"
|
||||||
},
|
}
|
||||||
"trustedDependencies": [
|
|
||||||
"spawn-sync"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 366 B After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 78 B After Width: | Height: | Size: 102 B |
|
Before Width: | Height: | Size: 668 B After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 488 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 654 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 332 B After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 72 B After Width: | Height: | Size: 92 B |
|
Before Width: | Height: | Size: 330 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 308 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 224 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 678 B After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 62 B After Width: | Height: | Size: 52 B |
|
Before Width: | Height: | Size: 512 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 208 B After Width: | Height: | Size: 562 B |
|
Before Width: | Height: | Size: 118 B After Width: | Height: | Size: 166 B |
|
Before Width: | Height: | Size: 206 B After Width: | Height: | Size: 556 B |
|
Before Width: | Height: | Size: 288 B After Width: | Height: | Size: 668 B |
|
Before Width: | Height: | Size: 216 B After Width: | Height: | Size: 692 B |
|
Before Width: | Height: | Size: 208 B After Width: | Height: | Size: 562 B |
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 104 B |
|
Before Width: | Height: | Size: 184 B After Width: | Height: | Size: 422 B |
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 76 B |
|
Before Width: | Height: | Size: 118 B After Width: | Height: | Size: 452 B |
|
Before Width: | Height: | Size: 514 B After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 64 B After Width: | Height: | Size: 90 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 878 B After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 878 B After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 444 B After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 520 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 594 B After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 240 B After Width: | Height: | Size: 454 B |
|
Before Width: | Height: | Size: 694 B After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 108 B After Width: | Height: | Size: 168 B |
|
Before Width: | Height: | Size: 58 B After Width: | Height: | Size: 100 B |
|
Before Width: | Height: | Size: 356 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 794 B After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 292 B After Width: | Height: | Size: 1002 B |
|
Before Width: | Height: | Size: 510 B After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 470 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 158 B After Width: | Height: | Size: 408 B |
|
Before Width: | Height: | Size: 96 B After Width: | Height: | Size: 166 B |
|
Before Width: | Height: | Size: 96 B After Width: | Height: | Size: 72 B |
|
Before Width: | Height: | Size: 58 B After Width: | Height: | Size: 96 B |
|
Before Width: | Height: | Size: 684 B After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 146 B After Width: | Height: | Size: 362 B |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 160 B After Width: | Height: | Size: 450 B |
|
Before Width: | Height: | Size: 204 B After Width: | Height: | Size: 790 B |
|
Before Width: | Height: | Size: 58 B After Width: | Height: | Size: 96 B |
|
Before Width: | Height: | Size: 328 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 330 B After Width: | Height: | Size: 858 B |
|
Before Width: | Height: | Size: 318 B After Width: | Height: | Size: 692 B |
|
Before Width: | Height: | Size: 190 B After Width: | Height: | Size: 474 B |
|
Before Width: | Height: | Size: 650 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 346 B After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 176 B After Width: | Height: | Size: 346 B |
|
Before Width: | Height: | Size: 56 B After Width: | Height: | Size: 62 B |
|
Before Width: | Height: | Size: 374 B After Width: | Height: | Size: 998 B |
|
Before Width: | Height: | Size: 98 B After Width: | Height: | Size: 124 B |
|
Before Width: | Height: | Size: 466 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 252 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 292 B After Width: | Height: | Size: 784 B |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 64 B After Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 202 B After Width: | Height: | Size: 944 B |
|
Before Width: | Height: | Size: 342 B After Width: | Height: | Size: 780 B |
|
Before Width: | Height: | Size: 430 B After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 460 B After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 468 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 94 B After Width: | Height: | Size: 120 B |
|
Before Width: | Height: | Size: 810 B After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 210 B After Width: | Height: | Size: 566 B |
|
Before Width: | Height: | Size: 112 B After Width: | Height: | Size: 164 B |
|
Before Width: | Height: | Size: 60 B After Width: | Height: | Size: 96 B |
|
Before Width: | Height: | Size: 72 B After Width: | Height: | Size: 78 B |
|
Before Width: | Height: | Size: 76 B After Width: | Height: | Size: 122 B |
|
Before Width: | Height: | Size: 500 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 342 B After Width: | Height: | Size: 926 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 336 B After Width: | Height: | Size: 512 B |
|
Before Width: | Height: | Size: 590 B After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 126 B After Width: | Height: | Size: 742 B |
|
Before Width: | Height: | Size: 272 B After Width: | Height: | Size: 708 B |