Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24c154a759 | ||
|
|
edf87370d9 | ||
|
|
ae0f9447ea | ||
|
|
972b6c7f22 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "litek",
|
"name": "litek",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.9",
|
"version": "0.0.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -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 />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -41,6 +41,26 @@ const Tool: FC = () => {
|
|||||||
const [results, setResults] = useState<DNSRecord[]>([]);
|
const [results, setResults] = useState<DNSRecord[]>([]);
|
||||||
const [queryTime, setQueryTime] = useState<number>(0);
|
const [queryTime, setQueryTime] = useState<number>(0);
|
||||||
|
|
||||||
|
const handleDomainBlur = () => {
|
||||||
|
if (!domain.trim()) return;
|
||||||
|
|
||||||
|
let input = domain.trim();
|
||||||
|
let cleanDomain = input;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to parse as URL
|
||||||
|
const url = new URL(input.startsWith('http') ? input : `https://${input}`);
|
||||||
|
cleanDomain = url.hostname;
|
||||||
|
} catch {
|
||||||
|
// If parsing fails, fallback to manual cleanup
|
||||||
|
cleanDomain = input.replace(/^https?:\/\//, "").split("/")[0].split(":")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanDomain !== input) {
|
||||||
|
setDomain(cleanDomain);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const queryDNS = async () => {
|
const queryDNS = async () => {
|
||||||
if (!domain.trim()) {
|
if (!domain.trim()) {
|
||||||
toast.error("Please enter a domain name");
|
toast.error("Please enter a domain name");
|
||||||
@@ -58,7 +78,7 @@ const Tool: FC = () => {
|
|||||||
const queries = DNS_RECORD_TYPES.map((recordType) =>
|
const queries = DNS_RECORD_TYPES.map((recordType) =>
|
||||||
fetch(
|
fetch(
|
||||||
`https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(
|
`https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(
|
||||||
domain
|
domain.trim()
|
||||||
)}&type=${recordType.value}`,
|
)}&type=${recordType.value}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
@@ -127,6 +147,7 @@ const Tool: FC = () => {
|
|||||||
placeholder="e.g. example.com"
|
placeholder="e.g. example.com"
|
||||||
value={domain}
|
value={domain}
|
||||||
onChange={(e) => setDomain(e.target.value)}
|
onChange={(e) => setDomain(e.target.value)}
|
||||||
|
onBlur={handleDomainBlur}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -36,6 +36,27 @@ const Tool: FC = () => {
|
|||||||
const seqRef = useRef<number>(0);
|
const seqRef = useRef<number>(0);
|
||||||
const resultsContainerRef = useRef<HTMLDivElement>(null);
|
const resultsContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleUrlBlur = () => {
|
||||||
|
if (!url.trim()) return;
|
||||||
|
|
||||||
|
let input = url.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to parse as URL
|
||||||
|
const parsedUrl = new URL(input.startsWith('http') ? input : `https://${input}`);
|
||||||
|
const normalizedUrl = parsedUrl.toString();
|
||||||
|
|
||||||
|
if (normalizedUrl !== input) {
|
||||||
|
setUrl(normalizedUrl);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If parsing fails, add https:// prefix
|
||||||
|
if (!input.startsWith("http://") && !input.startsWith("https://")) {
|
||||||
|
setUrl(`https://${input}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const ping = async () => {
|
const ping = async () => {
|
||||||
if (!url.trim()) {
|
if (!url.trim()) {
|
||||||
toast.error("Please enter a URL");
|
toast.error("Please enter a URL");
|
||||||
@@ -43,12 +64,7 @@ const Tool: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const seq = ++seqRef.current;
|
const seq = ++seqRef.current;
|
||||||
let targetUrl = url.trim();
|
const targetUrl = url.trim();
|
||||||
|
|
||||||
// If no protocol prefix, default to https://
|
|
||||||
if (!targetUrl.startsWith("http://") && !targetUrl.startsWith("https://")) {
|
|
||||||
targetUrl = `https://${targetUrl}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
|
|
||||||
@@ -177,6 +193,7 @@ const Tool: FC = () => {
|
|||||||
placeholder="e.g. example.com or https://example.com"
|
placeholder="e.g. example.com or https://example.com"
|
||||||
value={url}
|
value={url}
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
|
onBlur={handleUrlBlur}
|
||||||
disabled={running}
|
disabled={running}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -142,18 +142,34 @@ const Tool: FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUrlBlur = () => {
|
||||||
|
if (!url.trim()) return;
|
||||||
|
|
||||||
|
let input = url.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to parse as URL
|
||||||
|
const parsedUrl = new URL(input.startsWith('http') ? input : `https://${input}`);
|
||||||
|
const normalizedUrl = parsedUrl.toString();
|
||||||
|
|
||||||
|
if (normalizedUrl !== input) {
|
||||||
|
setUrl(normalizedUrl);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If parsing fails, add https:// prefix
|
||||||
|
if (!input.startsWith("http://") && !input.startsWith("https://")) {
|
||||||
|
setUrl(`https://${input}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const startTest = async () => {
|
const startTest = async () => {
|
||||||
if (!url.trim()) {
|
if (!url.trim()) {
|
||||||
toast.error("Please enter a URL");
|
toast.error("Please enter a URL");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetUrl = url.trim();
|
const targetUrl = url.trim();
|
||||||
|
|
||||||
// If no protocol prefix, default to https://
|
|
||||||
if (!targetUrl.startsWith("http://") && !targetUrl.startsWith("https://")) {
|
|
||||||
targetUrl = `https://${targetUrl}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTesting(true);
|
setTesting(true);
|
||||||
setResult(null);
|
setResult(null);
|
||||||
@@ -218,6 +234,7 @@ const Tool: FC = () => {
|
|||||||
placeholder="e.g. https://example.com"
|
placeholder="e.g. https://example.com"
|
||||||
value={url}
|
value={url}
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
|
onBlur={handleUrlBlur}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
disabled={testing}
|
disabled={testing}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -37,6 +37,46 @@ const Tool: FC = () => {
|
|||||||
const seqRef = useRef<number>(0);
|
const seqRef = useRef<number>(0);
|
||||||
const resultsContainerRef = useRef<HTMLDivElement>(null);
|
const resultsContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleHostBlur = () => {
|
||||||
|
if (!host.trim()) return;
|
||||||
|
|
||||||
|
let input = host.trim();
|
||||||
|
let cleanHost = input;
|
||||||
|
let extractedPort: string | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to parse as URL
|
||||||
|
const url = new URL(input.startsWith('http') ? input : `https://${input}`);
|
||||||
|
cleanHost = url.hostname;
|
||||||
|
|
||||||
|
// Extract port if specified in URL
|
||||||
|
if (url.port) {
|
||||||
|
extractedPort = url.port;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If parsing fails, fallback to manual cleanup
|
||||||
|
const withoutProtocol = input.replace(/^https?:\/\//, "");
|
||||||
|
const withoutPath = withoutProtocol.split("/")[0];
|
||||||
|
|
||||||
|
// Check for port in the format hostname:port
|
||||||
|
const portMatch = withoutPath.match(/^(.+):(\d+)$/);
|
||||||
|
if (portMatch) {
|
||||||
|
cleanHost = portMatch[1];
|
||||||
|
extractedPort = portMatch[2];
|
||||||
|
} else {
|
||||||
|
cleanHost = withoutPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanHost !== input) {
|
||||||
|
setHost(cleanHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extractedPort) {
|
||||||
|
setPort(extractedPort);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const tcping = async () => {
|
const tcping = async () => {
|
||||||
if (!host.trim()) {
|
if (!host.trim()) {
|
||||||
toast.error("Please enter a hostname or IP");
|
toast.error("Please enter a hostname or IP");
|
||||||
@@ -45,14 +85,11 @@ const Tool: FC = () => {
|
|||||||
|
|
||||||
const seq = ++seqRef.current;
|
const seq = ++seqRef.current;
|
||||||
const portNum = parseInt(port) || 443;
|
const portNum = parseInt(port) || 443;
|
||||||
let targetUrl = host.trim();
|
const targetHost = host.trim();
|
||||||
|
|
||||||
// 移除协议前缀
|
// Build test URL
|
||||||
targetUrl = targetUrl.replace(/^https?:\/\//, "");
|
|
||||||
|
|
||||||
// 构建测试 URL
|
|
||||||
const protocol = portNum === 443 ? "https" : "http";
|
const protocol = portNum === 443 ? "https" : "http";
|
||||||
const url = `${protocol}://${targetUrl}:${portNum}`;
|
const url = `${protocol}://${targetHost}:${portNum}`;
|
||||||
|
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
|
|
||||||
@@ -182,6 +219,7 @@ const Tool: FC = () => {
|
|||||||
placeholder="e.g. example.com or 192.168.1.1"
|
placeholder="e.g. example.com or 192.168.1.1"
|
||||||
value={host}
|
value={host}
|
||||||
onChange={(e) => setHost(e.target.value)}
|
onChange={(e) => setHost(e.target.value)}
|
||||||
|
onBlur={handleHostBlur}
|
||||||
disabled={running}
|
disabled={running}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user