Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
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 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://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,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,97 +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>
|
|
||||||
<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 (
|
// 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>
|
// Public Internet View
|
||||||
</div>
|
return <PublicNetworkView data={data} />;
|
||||||
<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,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()
|
|
||||||
|
|
||||||
await updateIcon(apiData.country || null)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to handle tab update:', error)
|
|
||||||
await updateIcon(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chrome.tabs.onActivated.addListener(async activeInfo => {
|
|
||||||
const tab = await chrome.tabs.get(activeInfo.tabId)
|
|
||||||
if (tab.url) await handleTabUpdate(tab.url)
|
|
||||||
})
|
|
||||||
|
|
||||||
chrome.tabs.onUpdated.addListener(async (_tabId, changeInfo) => {
|
|
||||||
if (changeInfo.url) await handleTabUpdate(changeInfo.url)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default defineBackground({
|
export default defineBackground({
|
||||||
main() {
|
main() {
|
||||||
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
|
// Listen for Network Responses
|
||||||
if (request.type === 'FETCH_SERVER_INFO') {
|
browser.webRequest.onResponseStarted.addListener(
|
||||||
;(async () => {
|
async (details) => {
|
||||||
|
if (details.tabId === -1 || details.type !== 'main_frame' || !details.ip) return;
|
||||||
|
await Tab.process(details.tabId, details.url, details.ip);
|
||||||
|
},
|
||||||
|
{ urls: ['<all_urls>'] }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Listen for Network Errors (DNS, Connection Refused, etc.)
|
||||||
|
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>'] }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Listen for Navigation
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (isSystemPage) {
|
||||||
|
await Tab.processSystemPage(tabId);
|
||||||
|
} else if (tab.url.startsWith('http')) {
|
||||||
|
// We might not have the IP yet if it was cached, so we trigger a process
|
||||||
|
// If IP is missing, Tab waits or we can force a HEAD request here
|
||||||
|
await Tab.process(tabId, tab.url);
|
||||||
|
|
||||||
|
// Force connection to ensure webRequest fires if cached
|
||||||
try {
|
try {
|
||||||
const ip = await resolveARecord(request.hostname)
|
await fetch(tab.url, { method: 'HEAD', cache: 'no-store', mode: 'no-cors' });
|
||||||
if (!ip) {
|
} catch { /* ignore */ }
|
||||||
sendResponse({ error: 'DNS resolution failed', data: null })
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const apiResponse = await fetch(`https://ip.albert.lol/${ip}`)
|
// Cleanup
|
||||||
const apiData = await apiResponse.json()
|
browser.tabs.onRemoved.addListener(async (tabId) => {
|
||||||
|
await StorageService.remove(tabId);
|
||||||
const parsed = psl.parse(request.hostname)
|
});
|
||||||
const origin = 'domain' in parsed ? parsed.domain : null
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
} catch (error) {
|
|
||||||
await updateIcon(null)
|
|
||||||
sendResponse({
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
data: null,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -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,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,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.1",
|
"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 |
|
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 |