diff --git a/src/components/tool/index.tsx b/src/components/tool/index.tsx index ead16c2..72d2443 100644 --- a/src/components/tool/index.tsx +++ b/src/components/tool/index.tsx @@ -1,10 +1,10 @@ import type { ReactNode } from 'react'; -import { FileJson, Hash, Binary, Network, Globe, Activity, Gauge, Wifi } from 'lucide-react' +import { FileJson, Hash, Binary, Network, Globe, Activity, Gauge, Wifi, MapPin } from 'lucide-react' import UUID from './uuid' import JSON from './json' import Base64 from './base64' -import { DNS, Ping, TCPing, SpeedTest } from './network' +import { DNS, Ping, TCPing, SpeedTest, IPQuery } from './network' export interface Tool { path: string; @@ -71,6 +71,13 @@ export const tools: Tool[] = [ icon: , component: , }, + { + path: "ipquery", + name: "IP Query", + description: "Query IP location, quality and risk info", + icon: , + component: , + }, ], }, ]; \ No newline at end of file diff --git a/src/components/tool/network/index.tsx b/src/components/tool/network/index.tsx index 380f532..cfda630 100644 --- a/src/components/tool/network/index.tsx +++ b/src/components/tool/network/index.tsx @@ -2,4 +2,5 @@ export { default as DNS } from './dns'; export { default as Ping } from './ping'; export { default as TCPing } from './tcping'; export { default as SpeedTest } from './speedtest'; +export { default as IPQuery } from './ipquery'; diff --git a/src/components/tool/network/ipquery.tsx b/src/components/tool/network/ipquery.tsx new file mode 100644 index 0000000..34abb8f --- /dev/null +++ b/src/components/tool/network/ipquery.tsx @@ -0,0 +1,312 @@ +import { useState, type FC } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { toast } from "sonner"; +import { Loader2 } from "lucide-react"; + +interface IPInfo { + ip: string; + city?: string; + region?: string; + country?: string; + countryCode?: string; + loc?: string; + org?: string; + timezone?: string; + isp?: string; + as?: string; + proxy?: boolean; + hosting?: boolean; + query?: string; +} + +const Tool: FC = () => { + const [ip, setIp] = useState(""); + const [loading, setLoading] = useState(false); + const [ipInfo, setIpInfo] = useState(null); + const [queryTime, setQueryTime] = useState(0); + + const isValidIP = (ip: string): boolean => { + // IPv4 正则 + const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/; + // IPv6 正则 (简化版) + const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/; + + if (ipv4Regex.test(ip)) { + const parts = ip.split('.'); + return parts.every(part => parseInt(part) >= 0 && parseInt(part) <= 255); + } + + return ipv6Regex.test(ip); + }; + + const queryCurrentIP = async () => { + setLoading(true); + setIpInfo(null); + setQueryTime(0); + + const startTime = performance.now(); + + try { + // 使用 ipinfo.io 查询当前IP (免费,无需密钥) + const response = await fetch("https://ipinfo.io/json"); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + const endTime = performance.now(); + + setQueryTime(endTime - startTime); + setIpInfo(data); + setIp(data.ip); + toast.success("Successfully queried current IP"); + } catch (error) { + if (error instanceof Error) { + toast.error(`Query failed: ${error.message}`); + } else { + toast.error("Query failed"); + } + } finally { + setLoading(false); + } + }; + + const queryIP = async () => { + if (!ip.trim()) { + toast.error("Please enter an IP address"); + return; + } + + if (!isValidIP(ip.trim())) { + toast.error("Invalid IP address format"); + return; + } + + setLoading(true); + setIpInfo(null); + setQueryTime(0); + + const startTime = performance.now(); + + try { + // 使用 ip-api.com (免费,功能较全) + const response = await fetch(`http://ip-api.com/json/${encodeURIComponent(ip.trim())}?fields=status,message,country,countryCode,region,city,lat,lon,timezone,isp,org,as,proxy,hosting,query`); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + const endTime = performance.now(); + + setQueryTime(endTime - startTime); + + if (data.status === "fail") { + toast.error(data.message || "Query failed"); + return; + } + + // 转换为统一格式 + const ipData: IPInfo = { + ip: data.query, + city: data.city, + region: data.region, + country: data.country, + countryCode: data.countryCode, + loc: data.lat && data.lon ? `${data.lat},${data.lon}` : undefined, + timezone: data.timezone, + isp: data.isp, + org: data.org, + as: data.as, + proxy: data.proxy, + hosting: data.hosting, + }; + + setIpInfo(ipData); + toast.success("Query successful"); + } catch (error) { + if (error instanceof Error) { + toast.error(`Query failed: ${error.message}`); + } else { + toast.error("Query failed"); + } + } finally { + setLoading(false); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !loading) { + queryIP(); + } + }; + + const getRiskLevel = () => { + if (!ipInfo) return null; + + if (ipInfo.proxy || ipInfo.hosting) { + return { + level: "High", + color: "text-red-500", + reasons: [ + ipInfo.proxy && "Proxy/VPN detected", + ipInfo.hosting && "Hosting/Datacenter IP", + ].filter(Boolean), + }; + } + + return { + level: "Low", + color: "text-green-500", + reasons: ["Regular residential IP"], + }; + }; + + const riskInfo = getRiskLevel(); + + return ( +
+
+
+ + setIp(e.target.value)} + onKeyPress={handleKeyPress} + disabled={loading} + /> + + Supports IPv4 and IPv6 addresses + +
+ +
+ + +
+
+ + {queryTime > 0 && ( +
+ Query time: {queryTime.toFixed(2)} ms +
+ )} + + {ipInfo && ( +
+
IP Information:
+ + {/* 基本信息 */} +
+
Basic Information
+
+
IP Address:
+
{ipInfo.ip || ipInfo.query}
+ + {ipInfo.country && ( + <> +
Country:
+
{ipInfo.country} ({ipInfo.countryCode})
+ + )} + + {ipInfo.region && ( + <> +
Region:
+
{ipInfo.region}
+ + )} + + {ipInfo.city && ( + <> +
City:
+
{ipInfo.city}
+ + )} + + {ipInfo.loc && ( + <> +
Coordinates:
+
{ipInfo.loc}
+ + )} + + {ipInfo.timezone && ( + <> +
Timezone:
+
{ipInfo.timezone}
+ + )} +
+
+ + {/* 网络信息 */} + {(ipInfo.isp || ipInfo.org || ipInfo.as) && ( +
+
Network Information
+
+ {ipInfo.isp && ( + <> +
ISP:
+
{ipInfo.isp}
+ + )} + + {ipInfo.org && ( + <> +
Organization:
+
{ipInfo.org}
+ + )} + + {ipInfo.as && ( + <> +
AS Number:
+
{ipInfo.as}
+ + )} +
+
+ )} + + {/* 风险评估 */} + {riskInfo && ( +
+
Risk Assessment
+
+
Risk Level:
+
+ {riskInfo.level} +
+ +
Details:
+
+ {riskInfo.reasons.map((reason, idx) => ( +
{reason}
+ ))} +
+
+
+ )} +
+ )} +
+ ); +}; + +export default Tool; +