Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8eda2eae99 | ||
|
|
99673913a6 | ||
|
|
83e48e3485 | ||
|
|
8607591871 | ||
|
|
24c154a759 | ||
|
|
edf87370d9 |
@@ -43,8 +43,8 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=registry,ref=${{ secrets.REGISTRY_ENDPOINT }}/${{ github.repository_owner }}/litek:buildcache
|
# cache-from: type=registry,ref=${{ secrets.REGISTRY_ENDPOINT }}/${{ github.repository_owner }}/litek:buildcache
|
||||||
cache-to: type=registry,ref=${{ secrets.REGISTRY_ENDPOINT }}/${{ github.repository_owner }}/litek:buildcache,mode=max
|
# cache-to: type=registry,ref=${{ secrets.REGISTRY_ENDPOINT }}/${{ github.repository_owner }}/litek:buildcache,mode=max
|
||||||
# platforms: linux/amd64,linux/arm64
|
# platforms: linux/amd64,linux/arm64
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "litek",
|
"name": "litek",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.10",
|
"version": "0.0.13",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
/* 默认浅色模式使用黑色 */
|
||||||
|
path {
|
||||||
|
fill: #000000;
|
||||||
|
}
|
||||||
|
/* 暗色模式使用白色 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path {
|
||||||
|
fill: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
<g id="SVGRepo_iconCarrier"> <path d="M12.6089 6.12601C12.6785 5.85878 12.5183 5.58573 12.251 5.51614C11.9838 5.44655 11.7108 5.60676 11.6412 5.87399L10.0842 11.8528L7.85646 12.5211C7.59196 12.6005 7.44187 12.8792 7.52122 13.1437C7.60057 13.4082 7.87931 13.5583 8.14381 13.479L9.78926 12.9853L8.51617 17.874C8.47715 18.0238 8.50974 18.1833 8.60443 18.3058C8.69911 18.4283 8.8452 18.5 9.00003 18.5H16C16.2762 18.5 16.5 18.2761 16.5 18C16.5 17.7239 16.2762 17.5 16 17.5H9.64691L10.9102 12.6491L13.1438 11.979C13.4083 11.8996 13.5584 11.6209 13.479 11.3564C13.3997 11.0919 13.121 10.9418 12.8565 11.0211L11.2051 11.5165L12.6089 6.12601Z"/> </g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,10 +1,10 @@
|
|||||||
import type { ReactNode } from 'react';
|
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 UUID from './uuid'
|
||||||
import JSON from './json'
|
import JSON from './json'
|
||||||
import Base64 from './base64'
|
import Base64 from './base64'
|
||||||
import { DNS, Ping, TCPing, SpeedTest } from './network'
|
import { DNS, Ping, TCPing, SpeedTest, IPQuery } from './network'
|
||||||
|
|
||||||
export interface Tool {
|
export interface Tool {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -71,6 +71,13 @@ export const tools: Tool[] = [
|
|||||||
icon: <Gauge />,
|
icon: <Gauge />,
|
||||||
component: <SpeedTest />,
|
component: <SpeedTest />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "ipquery",
|
||||||
|
name: "IP Query",
|
||||||
|
description: "Query IP location, quality and risk info",
|
||||||
|
icon: <MapPin />,
|
||||||
|
component: <IPQuery />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -2,4 +2,5 @@ export { default as DNS } from './dns';
|
|||||||
export { default as Ping } from './ping';
|
export { default as Ping } from './ping';
|
||||||
export { default as TCPing } from './tcping';
|
export { default as TCPing } from './tcping';
|
||||||
export { default as SpeedTest } from './speedtest';
|
export { default as SpeedTest } from './speedtest';
|
||||||
|
export { default as IPQuery } from './ipquery';
|
||||||
|
|
||||||
|
|||||||
312
src/components/tool/network/ipquery.tsx
Normal file
312
src/components/tool/network/ipquery.tsx
Normal file
@@ -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<string>("");
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [ipInfo, setIpInfo] = useState<IPInfo | null>(null);
|
||||||
|
const [queryTime, setQueryTime] = useState<number>(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<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<div className="flex flex-col gap-4 h-full">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">IP Address</label>
|
||||||
|
<Input
|
||||||
|
placeholder="e.g. 8.8.8.8 or leave empty for current IP"
|
||||||
|
value={ip}
|
||||||
|
onChange={(e) => setIp(e.target.value)}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Supports IPv4 and IPv6 addresses
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={queryIP} disabled={loading} className="flex-1">
|
||||||
|
{loading && <Loader2 className="mr-2 size-4 animate-spin" />}
|
||||||
|
{loading ? "Querying..." : "Query IP"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={queryCurrentIP}
|
||||||
|
disabled={loading}
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{loading && <Loader2 className="mr-2 size-4 animate-spin" />}
|
||||||
|
Query My IP
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{queryTime > 0 && (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Query time: {queryTime.toFixed(2)} ms
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ipInfo && (
|
||||||
|
<div className="flex flex-col gap-3 flex-1 overflow-auto">
|
||||||
|
<div className="text-sm font-medium">IP Information:</div>
|
||||||
|
|
||||||
|
{/* 基本信息 */}
|
||||||
|
<div className="border rounded-md p-4 bg-card text-card-foreground">
|
||||||
|
<div className="text-sm font-medium mb-3">Basic Information</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
|
<div className="text-muted-foreground">IP Address:</div>
|
||||||
|
<div className="font-mono">{ipInfo.ip || ipInfo.query}</div>
|
||||||
|
|
||||||
|
{ipInfo.country && (
|
||||||
|
<>
|
||||||
|
<div className="text-muted-foreground">Country:</div>
|
||||||
|
<div>{ipInfo.country} ({ipInfo.countryCode})</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ipInfo.region && (
|
||||||
|
<>
|
||||||
|
<div className="text-muted-foreground">Region:</div>
|
||||||
|
<div>{ipInfo.region}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ipInfo.city && (
|
||||||
|
<>
|
||||||
|
<div className="text-muted-foreground">City:</div>
|
||||||
|
<div>{ipInfo.city}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ipInfo.loc && (
|
||||||
|
<>
|
||||||
|
<div className="text-muted-foreground">Coordinates:</div>
|
||||||
|
<div className="font-mono">{ipInfo.loc}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ipInfo.timezone && (
|
||||||
|
<>
|
||||||
|
<div className="text-muted-foreground">Timezone:</div>
|
||||||
|
<div>{ipInfo.timezone}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 网络信息 */}
|
||||||
|
{(ipInfo.isp || ipInfo.org || ipInfo.as) && (
|
||||||
|
<div className="border rounded-md p-4 bg-card text-card-foreground">
|
||||||
|
<div className="text-sm font-medium mb-3">Network Information</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
|
{ipInfo.isp && (
|
||||||
|
<>
|
||||||
|
<div className="text-muted-foreground">ISP:</div>
|
||||||
|
<div>{ipInfo.isp}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ipInfo.org && (
|
||||||
|
<>
|
||||||
|
<div className="text-muted-foreground">Organization:</div>
|
||||||
|
<div>{ipInfo.org}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ipInfo.as && (
|
||||||
|
<>
|
||||||
|
<div className="text-muted-foreground">AS Number:</div>
|
||||||
|
<div className="font-mono">{ipInfo.as}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 风险评估 */}
|
||||||
|
{riskInfo && (
|
||||||
|
<div className="border rounded-md p-4 bg-card text-card-foreground">
|
||||||
|
<div className="text-sm font-medium mb-3">Risk Assessment</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
|
<div className="text-muted-foreground">Risk Level:</div>
|
||||||
|
<div className={`font-medium ${riskInfo.color}`}>
|
||||||
|
{riskInfo.level}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-muted-foreground">Details:</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{riskInfo.reasons.map((reason, idx) => (
|
||||||
|
<div key={idx} className="text-sm">{reason}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tool;
|
||||||
|
|
||||||
Reference in New Issue
Block a user