Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
283ebe4dcb
|
|||
|
b488b7576a
|
|||
|
3fcbab5d30
|
|||
|
a755d3f1c2
|
|||
|
9db73ee29c
|
|||
|
76758c4572
|
|||
|
55e97a68f6
|
|||
|
25d0865f89
|
|||
|
85942e5827
|
|||
|
7048baebc2
|
|||
|
b3b76366bf
|
|||
|
c564ac8b69
|
|||
|
2f16721a93
|
|||
|
56ed5909c7
|
|||
|
55b3b61bbf
|
|||
|
5c107c2f0d
|
|||
|
fea3cf5a62
|
|||
| e9315bec47 | |||
|
7d614a2a7e
|
|||
|
da23868817
|
|||
|
9292a4a6e2
|
|||
|
2973e038d6
|
|||
|
991cd10f40
|
|||
|
6bf00dc6e0
|
|||
|
a4ae08870d
|
|||
|
f2cc9a8c87
|
|||
|
bf5c5575f5
|
|||
|
6f0125896a
|
|||
| 9253e53ca1 | |||
|
d5a4fcbe5a
|
|||
|
c3be23d369
|
|||
|
90bb276622
|
|||
|
806072fbf1
|
|||
|
baa277fbd9
|
|||
|
3e9fe9a199
|
|||
|
a540f1eb0a
|
|||
|
940b8bbec4
|
|||
| 3419544ba4 | |||
| 87b4c93268 | |||
| feaaa65960 | |||
|
971a980def
|
@@ -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`.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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,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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 { 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +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);
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.tabs.onActivated.addListener(async activeInfo => {
|
async function updateState(tabId: number, updates: Partial<TabState>, expectedUrl?: string) {
|
||||||
const tab = await chrome.tabs.get(activeInfo.tabId)
|
let current = tabStates.get(tabId);
|
||||||
if (tab.url) await handleTabUpdate(tab.url)
|
if (!current) {
|
||||||
})
|
current = await StorageService.getTabState(tabId) || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
chrome.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() {
|
||||||
chrome.runtime.onMessage.addListener((request, _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') {
|
||||||
await updateIcon(apiData.country)
|
StorageService.cleanExpiredGeoCache().catch(console.error);
|
||||||
|
|
||||||
sendResponse({
|
|
||||||
error: null,
|
|
||||||
data: {
|
|
||||||
origin,
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.tabs.onReplaced.addListener((addedTabId, removedTabId) => {
|
||||||
|
tabStates.delete(removedTabId);
|
||||||
|
StorageService.removeTabState(removedTabId);
|
||||||
|
|
||||||
|
browser.tabs.get(addedTabId).then((tab) => {
|
||||||
|
if (tab.url) {
|
||||||
|
initTab(tab.id!, tab.url, true)
|
||||||
|
}
|
||||||
|
}).catch(() => { })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
browser.webNavigation.onBeforeNavigate.addListener((details) => {
|
||||||
|
if (details.frameId !== 0) return;
|
||||||
|
initTab(details.tabId, details.url, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.webNavigation.onHistoryStateUpdated.addListener((details) => {
|
||||||
|
if (details.frameId !== 0) return;
|
||||||
|
initTab(details.tabId, details.url, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.webNavigation.onCommitted.addListener((details) => {
|
||||||
|
if (details.frameId !== 0) return;
|
||||||
|
const state = tabStates.get(details.tabId);
|
||||||
|
if (state) {
|
||||||
|
applyIconForState(details.tabId, state);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.webRequest.onResponseStarted.addListener(
|
||||||
|
(details) => {
|
||||||
|
if (details.tabId === -1 || details.type !== 'main_frame') return;
|
||||||
|
if (details.ip) {
|
||||||
|
processIp(details.tabId, details.url, details.ip);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ urls: ['<all_urls>'] }
|
||||||
|
);
|
||||||
|
|
||||||
|
browser.webNavigation.onCompleted.addListener(async (details) => {
|
||||||
|
if (details.frameId !== 0) return;
|
||||||
|
|
||||||
|
const state = tabStates.get(details.tabId);
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
applyIconForState(details.tabId, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state && state.status === 'loading' && !state.data) {
|
||||||
|
let hostname = '';
|
||||||
|
try {
|
||||||
|
hostname = new URL(details.url).hostname;
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ip = await DnsService.resolve(hostname);
|
||||||
|
|
||||||
|
const currentState = tabStates.get(details.tabId);
|
||||||
|
if (currentState?.status !== 'loading' || currentState.url !== details.url) return;
|
||||||
|
|
||||||
|
if (ip) {
|
||||||
|
await processIp(details.tabId, details.url, ip);
|
||||||
|
} else {
|
||||||
|
await updateState(details.tabId, {
|
||||||
|
status: 'error',
|
||||||
|
errorMessage: 'Could not resolve host'
|
||||||
|
}, details.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.webRequest.onErrorOccurred.addListener(
|
||||||
|
async (details) => {
|
||||||
|
if (details.type !== 'main_frame') return;
|
||||||
|
if (details.error === 'net::ERR_ABORTED') {
|
||||||
|
try {
|
||||||
|
const tab = await browser.tabs.get(details.tabId);
|
||||||
|
if (tab.url) {
|
||||||
|
const currentState = tabStates.get(details.tabId);
|
||||||
|
if (currentState && currentState.url !== tab.url) {
|
||||||
|
initTab(tab.id!, tab.url, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateState(details.tabId, {
|
||||||
|
status: 'error',
|
||||||
|
errorMessage: details.error
|
||||||
|
}, details.url);
|
||||||
|
},
|
||||||
|
{ urls: ['<all_urls>'] }
|
||||||
|
);
|
||||||
|
|
||||||
|
browser.tabs.onRemoved.addListener((tabId) => {
|
||||||
|
tabStates.delete(tabId);
|
||||||
|
StorageService.removeTabState(tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.tabs.onActivated.addListener(async (activeInfo) => {
|
||||||
|
const tab = await browser.tabs.get(activeInfo.tabId);
|
||||||
|
if (tab.url) {
|
||||||
|
const state = tabStates.get(tab.id!);
|
||||||
|
if (state) {
|
||||||
|
applyIconForState(tab.id!, state);
|
||||||
|
if (state.status === 'loading' && Date.now() - state.lastUpdated > 2000) {
|
||||||
|
initTab(tab.id!, tab.url, true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
initTab(tab.id!, tab.url, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||||
|
if (changeInfo.status && tab.url) {
|
||||||
|
if (tab.url) {
|
||||||
|
const state = tabStates.get(tabId);
|
||||||
|
if (state) {
|
||||||
|
applyIconForState(tabId, state);
|
||||||
|
if (!state || state.url !== tab.url) {
|
||||||
|
initTab(tabId, tab.url, true)
|
||||||
|
} else {
|
||||||
|
applyIconForState(tabId, state);
|
||||||
|
if (changeInfo.status === 'complete' && state.status === 'loading') {
|
||||||
|
initTab(tabId, tab.url, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.runtime.onMessage.addListener((message) => {
|
||||||
|
if (message.type === 'INIT_TAB' && message.tabId && message.url) {
|
||||||
|
initTab(message.tabId, message.url, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -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} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
export function useTabData() {
|
|
||||||
const [data, setData] = useState<ServerData | null>(null)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const [tab] = await chrome.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 chrome.runtime.sendMessage({
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
@@ -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.1",
|
"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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 366 B |
|
After Width: | Height: | Size: 704 B |
|
Before Width: | Height: | Size: 78 B |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 668 B |
|
After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 488 B |
|
After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 654 B |
|
After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 466 B |
|
Before Width: | Height: | Size: 72 B |
|
After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 330 B |
|
After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 308 B |
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 224 B |
|
After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 678 B |
|
After Width: | Height: | Size: 470 B |
|
Before Width: | Height: | Size: 62 B |
|
After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 512 B |
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 208 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 118 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 206 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 288 B |
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 216 B |
|
After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 208 B |
|
After Width: | Height: | Size: 510 B |
|
Before Width: | Height: | Size: 70 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 184 B |
|
After Width: | Height: | Size: 492 B |
|
Before Width: | Height: | Size: 70 B |
|
After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 118 B |
|
After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 514 B |
|
After Width: | Height: | Size: 743 B |
|
Before Width: | Height: | Size: 64 B |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 878 B |
|
After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 878 B |
|
After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 444 B |
|
After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 520 B |
|
After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 594 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 240 B |
|
After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 694 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 108 B |
|
After Width: | Height: | Size: 490 B |
|
Before Width: | Height: | Size: 58 B |
|
After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 356 B |
|
After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 794 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 292 B |
|
After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 510 B |
|
After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 470 B |
|
After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 158 B |
|
After Width: | Height: | Size: 3.8 KiB |