feat: wait before checking other server ping
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 9m47s
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 9m47s
This commit is contained in:
@ -22,6 +22,11 @@ interface Server {
|
|||||||
http: boolean
|
http: boolean
|
||||||
tcp: boolean
|
tcp: boolean
|
||||||
}
|
}
|
||||||
|
portRestrictions?: {
|
||||||
|
allowedRanges?: Array<{ min: number; max: number }>
|
||||||
|
blockedPorts?: number[]
|
||||||
|
supportsAutoAssign?: boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const geoUrl = "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json"
|
const geoUrl = "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json"
|
||||||
@ -78,13 +83,20 @@ const fetchServers = async (): Promise<Server[]> => {
|
|||||||
http: true,
|
http: true,
|
||||||
tcp: true,
|
tcp: true,
|
||||||
},
|
},
|
||||||
|
portRestrictions: {
|
||||||
|
allowedRanges: [
|
||||||
|
{ min: 40000, max: 41000 },
|
||||||
|
],
|
||||||
|
blockedPorts: [22, 80, 443, 3306, 5432, 6379, 2200],
|
||||||
|
supportsAutoAssign: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "id",
|
id: "id",
|
||||||
name: "Indonesia",
|
name: "Indonesia",
|
||||||
location: "Bogor",
|
location: "Bogor",
|
||||||
subdomain: "id.tunnl.live",
|
subdomain: "id.tunnl.live",
|
||||||
coordinates: [106.8456, -6.5950],
|
coordinates: [106.8456, -6.595],
|
||||||
ping: null,
|
ping: null,
|
||||||
status: "online",
|
status: "online",
|
||||||
pingStatus: "idle",
|
pingStatus: "idle",
|
||||||
@ -92,6 +104,10 @@ const fetchServers = async (): Promise<Server[]> => {
|
|||||||
http: true,
|
http: true,
|
||||||
tcp: true,
|
tcp: true,
|
||||||
},
|
},
|
||||||
|
portRestrictions: {
|
||||||
|
blockedPorts: [22, 80, 443, 3306, 5432, 6379, 2200],
|
||||||
|
supportsAutoAssign: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -180,13 +196,18 @@ const testServerPing = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function TunnelConfig({ config, onConfigChange, selectedServer, onServerSelect }: TunnelConfigProps) {
|
export default function TunnelConfig({ config, onConfigChange, selectedServer, onServerSelect }: TunnelConfigProps) {
|
||||||
const [localConfig, setLocalConfig] = useState<TunnelConfig>(config)
|
const [localConfig, setLocalConfig] = useState<TunnelConfig>({
|
||||||
|
...config,
|
||||||
|
serverPort: config.type === "tcp" ? 0 : config.serverPort,
|
||||||
|
})
|
||||||
const [servers, setServers] = useState<Server[]>([])
|
const [servers, setServers] = useState<Server[]>([])
|
||||||
const [isLoadingServers, setIsLoadingServers] = useState(true)
|
const [isLoadingServers, setIsLoadingServers] = useState(true)
|
||||||
const [isTestingPings, setIsTestingPings] = useState(false)
|
const [isTestingPings, setIsTestingPings] = useState(false)
|
||||||
const [hasAutoTested, setHasAutoTested] = useState(false)
|
const [hasAutoTested, setHasAutoTested] = useState(false)
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
const [serverError, setServerError] = useState<string | null>(null)
|
const [serverError, setServerError] = useState<string | null>(null)
|
||||||
|
const [portError, setPortError] = useState<string | null>(null)
|
||||||
|
const [pendingServerSelection, setPendingServerSelection] = useState<Server | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadServers = async () => {
|
const loadServers = async () => {
|
||||||
@ -223,18 +244,39 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pingPromises = servers.map((server) => testServerPing(server))
|
const testedServers: Server[] = []
|
||||||
const results = await Promise.all(pingPromises)
|
|
||||||
|
|
||||||
const updatedServers = results.map((result) => ({
|
for (const server of servers) {
|
||||||
...result.server,
|
try {
|
||||||
ping: result.ping,
|
const result = await testServerPing(server)
|
||||||
pingStatus: result.status,
|
|
||||||
}))
|
|
||||||
|
|
||||||
setServers(updatedServers)
|
const updatedServer = {
|
||||||
|
...result.server,
|
||||||
|
ping: result.ping,
|
||||||
|
pingStatus: result.status,
|
||||||
|
}
|
||||||
|
|
||||||
const compatibleServers = updatedServers.filter(
|
testedServers.push(updatedServer)
|
||||||
|
|
||||||
|
setServers((prevServers) => prevServers.map((s) => (s.id === server.id ? updatedServer : s)))
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error testing ping for ${server.id}:`, error)
|
||||||
|
|
||||||
|
const failedServer = {
|
||||||
|
...server,
|
||||||
|
ping: null,
|
||||||
|
pingStatus: "timeout" as const,
|
||||||
|
}
|
||||||
|
|
||||||
|
testedServers.push(failedServer)
|
||||||
|
|
||||||
|
setServers((prevServers) => prevServers.map((s) => (s.id === server.id ? failedServer : s)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const compatibleServers = testedServers.filter(
|
||||||
(s) =>
|
(s) =>
|
||||||
s.pingStatus === "success" &&
|
s.pingStatus === "success" &&
|
||||||
s.ping !== null &&
|
s.ping !== null &&
|
||||||
@ -246,20 +288,16 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
const bestServer = compatibleServers.reduce((prev, current) =>
|
const bestServer = compatibleServers.reduce((prev, current) =>
|
||||||
prev.ping! < current.ping! ? prev : current,
|
prev.ping! < current.ping! ? prev : current,
|
||||||
)
|
)
|
||||||
onServerSelect(bestServer)
|
setPendingServerSelection(bestServer)
|
||||||
} else {
|
} else {
|
||||||
const successfulServers = updatedServers.filter((s) => s.pingStatus === "success" && s.ping !== null)
|
const successfulServers = testedServers.filter((s) => s.pingStatus === "success" && s.ping !== null)
|
||||||
if (successfulServers.length > 0) {
|
if (successfulServers.length > 0) {
|
||||||
const bestServer = successfulServers.reduce((prev, current) =>
|
const bestServer = successfulServers.reduce((prev, current) =>
|
||||||
prev.ping! < current.ping! ? prev : current,
|
prev.ping! < current.ping! ? prev : current,
|
||||||
)
|
)
|
||||||
onServerSelect(bestServer)
|
setPendingServerSelection(bestServer)
|
||||||
|
} else if (testedServers.length > 0) {
|
||||||
if (localConfig.type === "tcp" && !bestServer.capabilities.tcp) {
|
setPendingServerSelection(testedServers[0])
|
||||||
updateConfig({ type: "http", serverPort: 443 })
|
|
||||||
}
|
|
||||||
} else if (updatedServers.length > 0) {
|
|
||||||
onServerSelect(updatedServers[0])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -271,7 +309,18 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
|
|
||||||
autoTestPings()
|
autoTestPings()
|
||||||
}
|
}
|
||||||
}, [servers.length, isLoadingServers, hasAutoTested, onServerSelect, localConfig.type])
|
}, [servers, isLoadingServers, hasAutoTested, localConfig.type])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pendingServerSelection) {
|
||||||
|
onServerSelect(pendingServerSelection)
|
||||||
|
setPendingServerSelection(null)
|
||||||
|
|
||||||
|
if (localConfig.type === "tcp" && !pendingServerSelection.capabilities.tcp) {
|
||||||
|
updateConfig({ type: "http", serverPort: 443 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [pendingServerSelection, onServerSelect, localConfig.type])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedServer && localConfig.type === "tcp" && !selectedServer.capabilities.tcp) {
|
if (selectedServer && localConfig.type === "tcp" && !selectedServer.capabilities.tcp) {
|
||||||
@ -279,6 +328,39 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
}
|
}
|
||||||
}, [selectedServer, localConfig.type])
|
}, [selectedServer, localConfig.type])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedServer && localConfig.type === "tcp" && localConfig.serverPort !== 0) {
|
||||||
|
const error = validatePort(localConfig.serverPort, selectedServer)
|
||||||
|
setPortError(error)
|
||||||
|
} else {
|
||||||
|
setPortError(null)
|
||||||
|
}
|
||||||
|
}, [selectedServer, localConfig.serverPort, localConfig.type])
|
||||||
|
|
||||||
|
const validatePort = (port: number, server: Server): string | null => {
|
||||||
|
if (!server.portRestrictions) return null
|
||||||
|
|
||||||
|
const { allowedRanges, blockedPorts } = server.portRestrictions
|
||||||
|
|
||||||
|
if (blockedPorts && blockedPorts.includes(port)) {
|
||||||
|
return `Port ${port} is not available on this server`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowedRanges && allowedRanges.length > 0) {
|
||||||
|
const isInRange = allowedRanges.some((range) => port >= range.min && port <= range.max)
|
||||||
|
if (!isInRange) {
|
||||||
|
const rangeStrings = allowedRanges.map((r) => `${r.min}-${r.max}`)
|
||||||
|
return `Port must be within allowed ranges: ${rangeStrings.join(", ")}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port < 1024) {
|
||||||
|
return `Port ${port} is restricted. Please use a port number 1024 or higher.`
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const updateConfig = (updates: Partial<TunnelConfig>) => {
|
const updateConfig = (updates: Partial<TunnelConfig>) => {
|
||||||
const newConfig = { ...localConfig, ...updates }
|
const newConfig = { ...localConfig, ...updates }
|
||||||
|
|
||||||
@ -309,9 +391,10 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
if (server.pingStatus === "testing") return "text-gray-400"
|
if (server.pingStatus === "testing") return "text-gray-400"
|
||||||
if (server.pingStatus === "failed" || server.pingStatus === "timeout") return "text-red-400"
|
if (server.pingStatus === "failed" || server.pingStatus === "timeout") return "text-red-400"
|
||||||
if (server.pingStatus === "idle" || !server.ping) return "text-gray-400"
|
if (server.pingStatus === "idle" || !server.ping) return "text-gray-400"
|
||||||
if (server.ping < 50) return "text-green-400"
|
if (server.ping < 100) return "text-green-400"
|
||||||
if (server.ping < 100) return "text-yellow-400"
|
if (server.ping < 300) return "text-yellow-400"
|
||||||
if (server.ping < 150) return "text-orange-400"
|
if (server.ping < 500) return "text-orange-400"
|
||||||
|
if (server.ping < 1000) return "text-red-400"
|
||||||
return "text-red-400"
|
return "text-red-400"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,19 +413,21 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
if (server.pingStatus === "failed") return "Connection Failed"
|
if (server.pingStatus === "failed") return "Connection Failed"
|
||||||
if (server.pingStatus === "idle") return "Not tested"
|
if (server.pingStatus === "idle") return "Not tested"
|
||||||
if (!server.ping) return "Unknown"
|
if (!server.ping) return "Unknown"
|
||||||
if (server.ping < 50) return "Excellent"
|
if (server.ping < 100) return "Excellent"
|
||||||
if (server.ping < 100) return "Good"
|
if (server.ping < 300) return "Good"
|
||||||
if (server.ping < 150) return "Fair"
|
if (server.ping < 500) return "Fair"
|
||||||
return "Poor"
|
if (server.ping < 1000) return "Poor"
|
||||||
|
return "Very Poor"
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMarkerColor = (server: Server) => {
|
const getMarkerColor = (server: Server) => {
|
||||||
if (selectedServer?.id === server.id) return "#10b981"
|
if (selectedServer?.id === server.id) return "#10b981"
|
||||||
if (server.pingStatus === "failed" || server.pingStatus === "timeout") return "#ef4444"
|
if (server.pingStatus === "failed" || server.pingStatus === "timeout") return "#ef4444"
|
||||||
if (server.pingStatus === "success" && server.ping !== null) {
|
if (server.pingStatus === "success" && server.ping !== null) {
|
||||||
if (server.ping < 50) return "#10b981"
|
if (server.ping < 100) return "#10b981"
|
||||||
if (server.ping < 100) return "#eab308"
|
if (server.ping < 300) return "#eab308"
|
||||||
if (server.ping < 150) return "#f97316"
|
if (server.ping < 500) return "#f97316"
|
||||||
|
if (server.ping < 1000) return "#ef4444"
|
||||||
return "#ef4444"
|
return "#ef4444"
|
||||||
}
|
}
|
||||||
return "#6b7280"
|
return "#6b7280"
|
||||||
@ -352,9 +437,10 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
if (selectedServer?.id === server.id) return "#34d399"
|
if (selectedServer?.id === server.id) return "#34d399"
|
||||||
if (server.pingStatus === "failed" || server.pingStatus === "timeout") return "#f87171"
|
if (server.pingStatus === "failed" || server.pingStatus === "timeout") return "#f87171"
|
||||||
if (server.pingStatus === "success" && server.ping !== null) {
|
if (server.pingStatus === "success" && server.ping !== null) {
|
||||||
if (server.ping < 50) return "#34d399"
|
if (server.ping < 100) return "#34d399"
|
||||||
if (server.ping < 100) return "#facc15"
|
if (server.ping < 300) return "#facc15"
|
||||||
if (server.ping < 150) return "#fb923c"
|
if (server.ping < 500) return "#fb923c"
|
||||||
|
if (server.ping < 1000) return "#f87171"
|
||||||
return "#f87171"
|
return "#f87171"
|
||||||
}
|
}
|
||||||
return "#9ca3af"
|
return "#9ca3af"
|
||||||
@ -399,18 +485,25 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pingPromises = servers.map((server) => testServerPing(server))
|
for (const server of servers) {
|
||||||
const results = await Promise.all(pingPromises)
|
try {
|
||||||
|
const result = await testServerPing(server)
|
||||||
|
|
||||||
const updatedServers = results.map((result) => ({
|
setServers((prevServers) =>
|
||||||
...result.server,
|
prevServers.map((s) => (s.id === server.id ? { ...s, ping: result.ping, pingStatus: result.status } : s)),
|
||||||
ping: result.ping,
|
)
|
||||||
pingStatus: result.status,
|
|
||||||
}))
|
|
||||||
|
|
||||||
setServers(updatedServers)
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error testing ping for ${server.id}:`, error)
|
||||||
|
|
||||||
|
setServers((prevServers) =>
|
||||||
|
prevServers.map((s) => (s.id === server.id ? { ...s, ping: null, pingStatus: "timeout" } : s)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error testing pings:", error)
|
console.error("Error in sequential ping testing:", error)
|
||||||
} finally {
|
} finally {
|
||||||
setIsTestingPings(false)
|
setIsTestingPings(false)
|
||||||
}
|
}
|
||||||
@ -452,6 +545,19 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPortRestrictionInfo = (server: Server) => {
|
||||||
|
if (!server.portRestrictions) return "Ports: 1024+"
|
||||||
|
|
||||||
|
const { allowedRanges } = server.portRestrictions
|
||||||
|
|
||||||
|
if (allowedRanges && allowedRanges.length > 0) {
|
||||||
|
const ranges = allowedRanges.map((r) => `${r.min}-${r.max}`).join(", ")
|
||||||
|
return `Ports: ${ranges}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Ports: 1024+"
|
||||||
|
}
|
||||||
|
|
||||||
const compatibleServers = getCompatibleServers()
|
const compatibleServers = getCompatibleServers()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -647,7 +753,7 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
</ComposableMap>
|
</ComposableMap>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 md:grid-cols-3">
|
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-4">
|
||||||
{servers.map((server) => {
|
{servers.map((server) => {
|
||||||
const canSelect = canSelectServer(server)
|
const canSelect = canSelectServer(server)
|
||||||
const unavailableReason = getServerUnavailableReason(server)
|
const unavailableReason = getServerUnavailableReason(server)
|
||||||
@ -660,30 +766,32 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
onServerSelect(server)
|
onServerSelect(server)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`p-3 rounded-lg border transition-all duration-200 ${selectedServer?.id === server.id
|
className={`p-3 rounded-lg border transition-all duration-200 ${
|
||||||
|
selectedServer?.id === server.id
|
||||||
? "bg-emerald-950 border-emerald-500"
|
? "bg-emerald-950 border-emerald-500"
|
||||||
: !canSelect
|
: !canSelect
|
||||||
? "bg-red-950 border-red-800 cursor-not-allowed opacity-75"
|
? "bg-red-950 border-red-800 cursor-not-allowed opacity-75"
|
||||||
: "bg-gray-800 border-gray-700 hover:border-gray-600 cursor-pointer"
|
: "bg-gray-800 border-gray-700 hover:border-gray-600 cursor-pointer"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<h5 className="font-medium text-sm">{server.name}</h5>
|
<h5 className="font-medium text-sm">{server.name}</h5>
|
||||||
<div
|
<div
|
||||||
className={`w-2 h-2 rounded-full ${selectedServer?.id === server.id
|
className={`w-2 h-2 rounded-full ${
|
||||||
|
selectedServer?.id === server.id
|
||||||
? "bg-emerald-400"
|
? "bg-emerald-400"
|
||||||
: !canSelect
|
: !canSelect
|
||||||
? "bg-red-400"
|
? "bg-red-400"
|
||||||
: server.pingStatus === "success" && server.ping !== null
|
: server.pingStatus === "success" && server.ping !== null
|
||||||
? server.ping < 50
|
? server.ping < 100
|
||||||
? "bg-green-400"
|
? "bg-green-400"
|
||||||
: server.ping < 100
|
: server.ping < 300
|
||||||
? "bg-yellow-400"
|
? "bg-yellow-400"
|
||||||
: server.ping < 150
|
: server.ping < 500
|
||||||
? "bg-orange-400"
|
? "bg-orange-400"
|
||||||
: "bg-red-400"
|
: "bg-red-400"
|
||||||
: "bg-gray-600"
|
: "bg-gray-600"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-400 mb-1">{server.location}</p>
|
<p className="text-xs text-gray-400 mb-1">{server.location}</p>
|
||||||
@ -701,6 +809,12 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{server.capabilities.tcp && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<p className="text-xs text-gray-300">{getPortRestrictionInfo(server)}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs">Ping:</span>
|
<span className="text-xs">Ping:</span>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@ -745,8 +859,9 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
<label className="block text-sm font-medium mb-3">Forwarding Type</label>
|
<label className="block text-sm font-medium mb-3">Forwarding Type</label>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<label
|
<label
|
||||||
className={`flex items-center ${servers.some((s) => s.capabilities.http) ? "cursor-pointer" : "cursor-not-allowed opacity-50"
|
className={`flex items-center ${
|
||||||
}`}
|
servers.some((s) => s.capabilities.http) ? "cursor-pointer" : "cursor-not-allowed opacity-50"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@ -760,20 +875,22 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
className="sr-only"
|
className="sr-only"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg border transition-all ${localConfig.type === "http"
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg border transition-all ${
|
||||||
|
localConfig.type === "http"
|
||||||
? "bg-emerald-950 border-emerald-500 text-emerald-400"
|
? "bg-emerald-950 border-emerald-500 text-emerald-400"
|
||||||
: servers.some((s) => s.capabilities.http)
|
: servers.some((s) => s.capabilities.http)
|
||||||
? "bg-gray-800 border-gray-700 text-gray-300 hover:border-gray-600"
|
? "bg-gray-800 border-gray-700 text-gray-300 hover:border-gray-600"
|
||||||
: "bg-gray-800 border-gray-700 text-gray-500 cursor-not-allowed"
|
: "bg-gray-800 border-gray-700 text-gray-500 cursor-not-allowed"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`w-2 h-2 rounded-full ${localConfig.type === "http"
|
className={`w-2 h-2 rounded-full ${
|
||||||
|
localConfig.type === "http"
|
||||||
? "bg-emerald-400"
|
? "bg-emerald-400"
|
||||||
: servers.some((s) => s.capabilities.http)
|
: servers.some((s) => s.capabilities.http)
|
||||||
? "bg-gray-500"
|
? "bg-gray-500"
|
||||||
: "bg-gray-600"
|
: "bg-gray-600"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span className="font-medium">HTTP/HTTPS</span>
|
<span className="font-medium">HTTP/HTTPS</span>
|
||||||
{!servers.some((s) => s.capabilities.http) && (
|
{!servers.some((s) => s.capabilities.http) && (
|
||||||
@ -783,8 +900,9 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
className={`flex items-center ${servers.some((s) => s.capabilities.tcp) ? "cursor-pointer" : "cursor-not-allowed opacity-50"
|
className={`flex items-center ${
|
||||||
}`}
|
servers.some((s) => s.capabilities.tcp) ? "cursor-pointer" : "cursor-not-allowed opacity-50"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@ -792,26 +910,28 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
value="tcp"
|
value="tcp"
|
||||||
checked={localConfig.type === "tcp"}
|
checked={localConfig.type === "tcp"}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
servers.some((s) => s.capabilities.tcp) && updateConfig({ type: "tcp", serverPort: 8080 })
|
servers.some((s) => s.capabilities.tcp) && updateConfig({ type: "tcp", serverPort: 0 })
|
||||||
}
|
}
|
||||||
disabled={!servers.some((s) => s.capabilities.tcp)}
|
disabled={!servers.some((s) => s.capabilities.tcp)}
|
||||||
className="sr-only"
|
className="sr-only"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg border transition-all ${localConfig.type === "tcp"
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg border transition-all ${
|
||||||
|
localConfig.type === "tcp"
|
||||||
? "bg-emerald-950 border-emerald-500 text-emerald-400"
|
? "bg-emerald-950 border-emerald-500 text-emerald-400"
|
||||||
: servers.some((s) => s.capabilities.tcp)
|
: servers.some((s) => s.capabilities.tcp)
|
||||||
? "bg-gray-800 border-gray-700 text-gray-300 hover:border-gray-600"
|
? "bg-gray-800 border-gray-700 text-gray-300 hover:border-gray-600"
|
||||||
: "bg-gray-800 border-gray-700 text-gray-500 cursor-not-allowed"
|
: "bg-gray-800 border-gray-700 text-gray-500 cursor-not-allowed"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`w-2 h-2 rounded-full ${localConfig.type === "tcp"
|
className={`w-2 h-2 rounded-full ${
|
||||||
|
localConfig.type === "tcp"
|
||||||
? "bg-emerald-400"
|
? "bg-emerald-400"
|
||||||
: servers.some((s) => s.capabilities.tcp)
|
: servers.some((s) => s.capabilities.tcp)
|
||||||
? "bg-gray-500"
|
? "bg-gray-500"
|
||||||
: "bg-gray-600"
|
: "bg-gray-600"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span className="font-medium">TCP</span>
|
<span className="font-medium">TCP</span>
|
||||||
{!servers.some((s) => s.capabilities.tcp) && (
|
{!servers.some((s) => s.capabilities.tcp) && (
|
||||||
@ -867,18 +987,30 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
<option value={80}>80 (HTTP)</option>
|
<option value={80}>80 (HTTP)</option>
|
||||||
</select>
|
</select>
|
||||||
) : (
|
) : (
|
||||||
<input
|
<div className="space-y-2">
|
||||||
type="number"
|
<input
|
||||||
value={localConfig.serverPort}
|
type="number"
|
||||||
onChange={(e) => updateConfig({ serverPort: Number.parseInt(e.target.value) || 8080 })}
|
value={localConfig.serverPort === 0 ? "" : localConfig.serverPort}
|
||||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white font-mono focus:border-emerald-500 focus:outline-none"
|
onChange={(e) => updateConfig({ serverPort: Number.parseInt(e.target.value) || 0 })}
|
||||||
placeholder="8080"
|
className={`w-full bg-gray-800 border rounded-lg px-3 py-2 text-white font-mono focus:outline-none ${
|
||||||
min="1024"
|
portError ? "border-red-500 focus:border-red-400" : "border-gray-700 focus:border-emerald-500"
|
||||||
max="65535"
|
}`}
|
||||||
/>
|
placeholder="0 for auto-assign"
|
||||||
|
min="0"
|
||||||
|
max="65535"
|
||||||
|
/>
|
||||||
|
{portError && <p className="text-xs text-red-400">{portError}</p>}
|
||||||
|
{localConfig.serverPort === 0 && (
|
||||||
|
<p className="text-xs text-blue-400">Server will automatically assign an available port</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<p className="text-xs text-gray-400 mt-1">
|
<p className="text-xs text-gray-400 mt-1">
|
||||||
{localConfig.type === "http" ? "Standard web ports" : "Port accessible from the internet"}
|
{localConfig.type === "http"
|
||||||
|
? "Standard web ports"
|
||||||
|
: localConfig.serverPort === 0
|
||||||
|
? "Server will assign an available port automatically"
|
||||||
|
: "Port accessible from the internet (1024+)"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -950,7 +1082,8 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
<p className="text-sm text-gray-300">
|
<p className="text-sm text-gray-300">
|
||||||
<span className="font-medium">Traffic Flow:</span> Internet →{" "}
|
<span className="font-medium">Traffic Flow:</span> Internet →{" "}
|
||||||
<span className="text-emerald-400 font-mono">
|
<span className="text-emerald-400 font-mono">
|
||||||
{selectedServer ? selectedServer.location : "Server"}:{localConfig.serverPort}
|
{selectedServer ? selectedServer.location : "Server"}:
|
||||||
|
{localConfig.serverPort === 0 ? "auto" : localConfig.serverPort}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
→ <span className="text-emerald-400 font-mono">localhost:{localConfig.localPort}</span>
|
→ <span className="text-emerald-400 font-mono">localhost:{localConfig.localPort}</span>
|
||||||
</p>
|
</p>
|
||||||
@ -962,7 +1095,8 @@ export default function TunnelConfig({ config, onConfigChange, selectedServer, o
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
TCP traffic to server port {localConfig.serverPort} will be forwarded to your localhost:
|
TCP traffic to server port {localConfig.serverPort === 0 ? "(auto-assigned)" : localConfig.serverPort}{" "}
|
||||||
|
will be forwarded to your localhost:
|
||||||
{localConfig.localPort}
|
{localConfig.localPort}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -1,198 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
|
||||||
import { ComposableMap, Geographies, Geography, Marker } from "react-simple-maps"
|
|
||||||
|
|
||||||
interface Server {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
location: string
|
|
||||||
subdomain: string
|
|
||||||
coordinates: [number, number] // [longitude, latitude]
|
|
||||||
ping: number | null
|
|
||||||
}
|
|
||||||
|
|
||||||
const servers: Server[] = [
|
|
||||||
{
|
|
||||||
id: "us",
|
|
||||||
name: "United States",
|
|
||||||
location: "Chicago",
|
|
||||||
subdomain: "us.tunnl.live",
|
|
||||||
coordinates: [-87.6298, 41.8781],
|
|
||||||
ping: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "eu",
|
|
||||||
name: "Europe",
|
|
||||||
location: "Frankfurt",
|
|
||||||
subdomain: "eu.tunnl.live",
|
|
||||||
coordinates: [8.6821, 50.1109],
|
|
||||||
ping: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "sgp",
|
|
||||||
name: "Singapore",
|
|
||||||
location: "Singapore",
|
|
||||||
subdomain: "sgp.tunnl.live",
|
|
||||||
coordinates: [103.8198, 1.3521],
|
|
||||||
ping: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "id",
|
|
||||||
name: "Indonesia",
|
|
||||||
location: "Bogor",
|
|
||||||
subdomain: "id.tunnl.live",
|
|
||||||
coordinates: [106.8456, -6.5950],
|
|
||||||
ping: null,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const geoUrl = "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json"
|
|
||||||
|
|
||||||
interface WorldMapProps {
|
|
||||||
onServerSelect: (server: Server) => void
|
|
||||||
selectedServer: Server
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function WorldMap({ onServerSelect, selectedServer }: WorldMapProps) {
|
|
||||||
const [serverPings, setServerPings] = useState<Server[]>(servers)
|
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
|
||||||
|
|
||||||
const getPingColor = (ping: number | null) => {
|
|
||||||
if (!ping) return "text-gray-400"
|
|
||||||
if (ping < 100) return "text-green-400"
|
|
||||||
if (ping < 300) return "text-yellow-400"
|
|
||||||
if (ping < 500) return "text-orange-400"
|
|
||||||
return "text-red-400"
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPingStatus = (ping: number | null) => {
|
|
||||||
if (!ping) return "Testing..."
|
|
||||||
if (ping < 100) return "Excellent"
|
|
||||||
if (ping < 300) return "Good"
|
|
||||||
if (ping < 500) return "Fair"
|
|
||||||
return "Poor"
|
|
||||||
}
|
|
||||||
|
|
||||||
const getMarkerColor = (server: Server) => {
|
|
||||||
if (selectedServer.id === server.id) return "#10b981"
|
|
||||||
return "#6b7280"
|
|
||||||
}
|
|
||||||
|
|
||||||
const getMarkerStroke = (server: Server) => {
|
|
||||||
if (selectedServer.id === server.id) return "#34d399"
|
|
||||||
return "#9ca3af"
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<h3 className="text-xl font-bold text-center mb-6">Choose Your Server Location</h3>
|
|
||||||
|
|
||||||
<div className="relative bg-gray-900 rounded-lg border border-gray-800 p-4 mb-6 overflow-hidden">
|
|
||||||
<ComposableMap
|
|
||||||
projection="geoMercator"
|
|
||||||
projectionConfig={{
|
|
||||||
scale: 120,
|
|
||||||
center: [0, 20],
|
|
||||||
}}
|
|
||||||
width={800}
|
|
||||||
height={400}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Geographies geography={geoUrl}>
|
|
||||||
{({ geographies }) =>
|
|
||||||
geographies.map((geo) => (
|
|
||||||
<Geography
|
|
||||||
key={geo.rsmKey}
|
|
||||||
geography={geo}
|
|
||||||
fill="#374151"
|
|
||||||
stroke="#4b5563"
|
|
||||||
strokeWidth={0.5}
|
|
||||||
style={{
|
|
||||||
default: { outline: "none" },
|
|
||||||
hover: { outline: "none", fill: "#4b5563" },
|
|
||||||
pressed: { outline: "none" },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</Geographies>
|
|
||||||
|
|
||||||
{serverPings.map((server) => (
|
|
||||||
<Marker
|
|
||||||
key={server.id}
|
|
||||||
coordinates={server.coordinates}
|
|
||||||
onClick={() => onServerSelect(server)}
|
|
||||||
>
|
|
||||||
<g>
|
|
||||||
{selectedServer.id === server.id && (
|
|
||||||
<circle r="15" fill="none" stroke="#10b981" strokeWidth="2" opacity="0.6">
|
|
||||||
<animate attributeName="r" values="8;20;8" dur="2s" repeatCount="indefinite" />
|
|
||||||
<animate attributeName="opacity" values="0.6;0;0.6" dur="2s" repeatCount="indefinite" />
|
|
||||||
</circle>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<circle
|
|
||||||
r="8"
|
|
||||||
fill={getMarkerColor(server)}
|
|
||||||
stroke={getMarkerStroke(server)}
|
|
||||||
strokeWidth="2"
|
|
||||||
className="transition-all duration-200 hover:r-10"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<text
|
|
||||||
textAnchor="middle"
|
|
||||||
y="-15"
|
|
||||||
style={{
|
|
||||||
fontFamily: "system-ui",
|
|
||||||
fontSize: "12px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
fill: "white",
|
|
||||||
pointerEvents: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{server.location}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
</Marker>
|
|
||||||
))}
|
|
||||||
</ComposableMap>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-3">
|
|
||||||
{serverPings.map((server) => (
|
|
||||||
<div
|
|
||||||
key={server.id}
|
|
||||||
onClick={() => onServerSelect(server)}
|
|
||||||
className={`p-4 rounded-lg border cursor-pointer transition-all duration-200 ${selectedServer.id === server.id
|
|
||||||
? "bg-emerald-950 border-emerald-500"
|
|
||||||
: "bg-gray-900 border-gray-800 hover:border-gray-700"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h4 className="font-bold">{server.name}</h4>
|
|
||||||
<div
|
|
||||||
className={`w-3 h-3 rounded-full ${selectedServer.id === server.id ? "bg-emerald-400" : "bg-gray-600"}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-400 mb-2">{server.location}</p>
|
|
||||||
<p className="text-xs font-mono text-gray-500 mb-2">{server.subdomain}</p>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-sm">Ping:</span>
|
|
||||||
{isLoading ? (
|
|
||||||
<span className="text-sm text-gray-400">Testing...</span>
|
|
||||||
) : (
|
|
||||||
<span className={`text-sm font-bold ${getPingColor(server.ping)}`}>
|
|
||||||
{server.ping}ms ({getPingStatus(server.ping)})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
Reference in New Issue
Block a user