772 lines
28 KiB
TypeScript
772 lines
28 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { ComposableMap, Geographies, Geography, Marker } from "react-simple-maps"
|
|
|
|
export interface TunnelConfig {
|
|
type: "http" | "tcp"
|
|
serverPort: number
|
|
localPort: number
|
|
}
|
|
|
|
interface Server {
|
|
id: string
|
|
name: string
|
|
location: string
|
|
subdomain: string
|
|
coordinates: [number, number]
|
|
ping: number | null
|
|
status: "online" | "offline" | "maintenance"
|
|
pingStatus: "idle" | "testing" | "success" | "failed" | "timeout"
|
|
}
|
|
|
|
const geoUrl = "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json"
|
|
|
|
interface TunnelConfigProps {
|
|
config: TunnelConfig
|
|
onConfigChange: (config: TunnelConfig) => void
|
|
selectedServer: Server | null
|
|
onServerSelect: (server: Server) => void
|
|
}
|
|
|
|
const fetchServers = async (): Promise<Server[]> => {
|
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
|
|
const mockServers: Server[] = [
|
|
{
|
|
id: "sgp",
|
|
name: "Singapore",
|
|
location: "Singapore",
|
|
subdomain: "sgp.tunnl.live",
|
|
coordinates: [103.8198, 1.3521],
|
|
ping: null,
|
|
status: "online",
|
|
pingStatus: "idle",
|
|
},
|
|
{
|
|
id: "id",
|
|
name: "Indonesia",
|
|
location: "Jakarta",
|
|
subdomain: "id.tunnl.live",
|
|
coordinates: [106.8456, -6.2088],
|
|
ping: null,
|
|
status: "online",
|
|
pingStatus: "idle",
|
|
},
|
|
]
|
|
|
|
return mockServers.filter((server) => server.status === "online")
|
|
}
|
|
|
|
const testServerPing = (
|
|
server: Server,
|
|
): Promise<{ server: Server; ping: number | null; status: Server["pingStatus"] }> => {
|
|
return new Promise((resolve) => {
|
|
const startTime = Date.now()
|
|
const timeout = 5000
|
|
let resolved = false
|
|
|
|
const pingUrl = `wss://ping.${server.subdomain}`
|
|
|
|
try {
|
|
const ws = new WebSocket(pingUrl)
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
if (!resolved) {
|
|
resolved = true
|
|
ws.close()
|
|
resolve({
|
|
server,
|
|
ping: null,
|
|
status: "timeout",
|
|
})
|
|
}
|
|
}, timeout)
|
|
|
|
ws.onopen = () => {
|
|
console.log(`Connected to ${pingUrl}`)
|
|
}
|
|
|
|
ws.onmessage = (event) => {
|
|
if (event.data === "pong" && !resolved) {
|
|
resolved = true
|
|
const ping = Date.now() - startTime
|
|
clearTimeout(timeoutId)
|
|
ws.close()
|
|
|
|
resolve({
|
|
server,
|
|
ping,
|
|
status: "success",
|
|
})
|
|
}
|
|
}
|
|
|
|
ws.onclose = (event) => {
|
|
if (!resolved) {
|
|
resolved = true
|
|
clearTimeout(timeoutId)
|
|
|
|
resolve({
|
|
server,
|
|
ping: null,
|
|
status: "failed",
|
|
})
|
|
}
|
|
}
|
|
|
|
ws.onerror = (error) => {
|
|
if (!resolved) {
|
|
resolved = true
|
|
clearTimeout(timeoutId)
|
|
console.error(`WebSocket error for ${pingUrl}:`, error)
|
|
|
|
resolve({
|
|
server,
|
|
ping: null,
|
|
status: "failed",
|
|
})
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(`Failed to create WebSocket for ${pingUrl}:`, error)
|
|
resolve({
|
|
server,
|
|
ping: null,
|
|
status: "failed",
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
export default function TunnelConfig({ config, onConfigChange, selectedServer, onServerSelect }: TunnelConfigProps) {
|
|
const [localConfig, setLocalConfig] = useState<TunnelConfig>(config)
|
|
const [servers, setServers] = useState<Server[]>([])
|
|
const [isLoadingServers, setIsLoadingServers] = useState(true)
|
|
const [isTestingPings, setIsTestingPings] = useState(false)
|
|
const [hasAutoTested, setHasAutoTested] = useState(false)
|
|
const [copied, setCopied] = useState(false)
|
|
const [serverError, setServerError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
const loadServers = async () => {
|
|
try {
|
|
setIsLoadingServers(true)
|
|
setServerError(null)
|
|
const serverData = await fetchServers()
|
|
setServers(serverData)
|
|
|
|
if (serverData.length === 0) {
|
|
setServerError("No servers are currently available. Please try again later.")
|
|
}
|
|
} catch (error) {
|
|
setServerError("Failed to load servers. Please check your connection and try again.")
|
|
} finally {
|
|
setIsLoadingServers(false)
|
|
}
|
|
}
|
|
|
|
loadServers()
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (servers.length > 0 && !isLoadingServers && !hasAutoTested) {
|
|
const autoTestPings = async () => {
|
|
setIsTestingPings(true)
|
|
setHasAutoTested(true)
|
|
|
|
setServers((prevServers) =>
|
|
prevServers.map((server) => ({
|
|
...server,
|
|
pingStatus: "testing" as const,
|
|
})),
|
|
)
|
|
|
|
try {
|
|
const pingPromises = servers.map((server) => testServerPing(server))
|
|
const results = await Promise.all(pingPromises)
|
|
|
|
const updatedServers = results.map((result) => ({
|
|
...result.server,
|
|
ping: result.ping,
|
|
pingStatus: result.status,
|
|
}))
|
|
|
|
setServers(updatedServers)
|
|
|
|
const successfulServers = updatedServers.filter((s) => s.pingStatus === "success" && s.ping !== null)
|
|
if (successfulServers.length > 0) {
|
|
const bestServer = successfulServers.reduce((prev, current) =>
|
|
prev.ping! < current.ping! ? prev : current,
|
|
)
|
|
onServerSelect(bestServer)
|
|
} else if (updatedServers.length > 0) {
|
|
onServerSelect(updatedServers[0])
|
|
}
|
|
} catch (error) {
|
|
console.error("Error testing pings:", error)
|
|
} finally {
|
|
setIsTestingPings(false)
|
|
}
|
|
}
|
|
|
|
autoTestPings()
|
|
}
|
|
}, [servers.length, isLoadingServers, hasAutoTested, onServerSelect])
|
|
|
|
const updateConfig = (updates: Partial<TunnelConfig>) => {
|
|
const newConfig = { ...localConfig, ...updates }
|
|
|
|
if (updates.serverPort && (updates.serverPort === 80 || updates.serverPort === 443)) {
|
|
newConfig.type = "http"
|
|
}
|
|
|
|
setLocalConfig(newConfig)
|
|
onConfigChange(newConfig)
|
|
}
|
|
|
|
const generateCommand = () => {
|
|
if (!selectedServer) return ""
|
|
const { serverPort, localPort } = localConfig
|
|
return `ssh ${selectedServer.subdomain} -p 2200 -R ${serverPort}:localhost:${localPort}`
|
|
}
|
|
|
|
const copyToClipboard = () => {
|
|
const command = generateCommand()
|
|
if (command) {
|
|
navigator.clipboard.writeText(command)
|
|
setCopied(true)
|
|
setTimeout(() => setCopied(false), 2000)
|
|
}
|
|
}
|
|
|
|
const getPingColor = (server: Server) => {
|
|
if (server.pingStatus === "testing") return "text-gray-400"
|
|
if (server.pingStatus === "failed" || server.pingStatus === "timeout") return "text-red-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-yellow-400"
|
|
if (server.ping < 150) return "text-orange-400"
|
|
return "text-red-400"
|
|
}
|
|
|
|
const getPingDisplay = (server: Server) => {
|
|
if (server.pingStatus === "testing") return "Testing..."
|
|
if (server.pingStatus === "timeout") return "Timeout"
|
|
if (server.pingStatus === "failed") return "Failed"
|
|
if (server.pingStatus === "idle") return "Click to test"
|
|
if (server.ping === null) return "N/A"
|
|
return `${server.ping}ms`
|
|
}
|
|
|
|
const getPingStatus = (server: Server) => {
|
|
if (server.pingStatus === "testing") return "Testing..."
|
|
if (server.pingStatus === "timeout") return "Timeout (5s)"
|
|
if (server.pingStatus === "failed") return "Connection Failed"
|
|
if (server.pingStatus === "idle") return "Not tested"
|
|
if (!server.ping) return "Unknown"
|
|
if (server.ping < 50) return "Excellent"
|
|
if (server.ping < 100) return "Good"
|
|
if (server.ping < 150) return "Fair"
|
|
return "Poor"
|
|
}
|
|
|
|
const getMarkerColor = (server: Server) => {
|
|
if (selectedServer?.id === server.id) return "#10b981"
|
|
if (server.pingStatus === "failed" || server.pingStatus === "timeout") return "#ef4444"
|
|
if (server.pingStatus === "success" && server.ping !== null) {
|
|
if (server.ping < 50) return "#10b981"
|
|
if (server.ping < 100) return "#eab308"
|
|
if (server.ping < 150) return "#f97316"
|
|
return "#ef4444"
|
|
}
|
|
return "#6b7280"
|
|
}
|
|
|
|
const getMarkerStroke = (server: Server) => {
|
|
if (selectedServer?.id === server.id) return "#34d399"
|
|
if (server.pingStatus === "failed" || server.pingStatus === "timeout") return "#f87171"
|
|
if (server.pingStatus === "success" && server.ping !== null) {
|
|
if (server.ping < 50) return "#34d399"
|
|
if (server.ping < 100) return "#facc15"
|
|
if (server.ping < 150) return "#fb923c"
|
|
return "#f87171"
|
|
}
|
|
return "#9ca3af"
|
|
}
|
|
|
|
const testPingForServer = async (server: Server) => {
|
|
setServers((prevServers) =>
|
|
prevServers.map((s) => (s.id === server.id ? { ...s, pingStatus: "testing", ping: null } : s)),
|
|
)
|
|
|
|
try {
|
|
const timeoutPromise = new Promise<{ server: Server; ping: number | null; status: Server["pingStatus"] }>(
|
|
(_, reject) => {
|
|
setTimeout(() => reject(new Error("Timeout")), 5000)
|
|
},
|
|
)
|
|
|
|
const result = await Promise.race([testServerPing(server), timeoutPromise])
|
|
|
|
setServers((prevServers) =>
|
|
prevServers.map((s) => (s.id === server.id ? { ...s, ping: result.ping, pingStatus: result.status } : s)),
|
|
)
|
|
} catch (error) {
|
|
console.error("Error testing ping:", error)
|
|
setServers((prevServers) =>
|
|
prevServers.map((s) => (s.id === server.id ? { ...s, ping: null, pingStatus: "timeout" } : s)),
|
|
)
|
|
}
|
|
}
|
|
|
|
const testAllPings = async () => {
|
|
if (servers.length === 0 || isTestingPings) return
|
|
|
|
setIsTestingPings(true)
|
|
|
|
setServers((prevServers) =>
|
|
prevServers.map((server) => ({
|
|
...server,
|
|
ping: null,
|
|
pingStatus: "testing" as const,
|
|
})),
|
|
)
|
|
|
|
try {
|
|
const pingPromises = servers.map((server) => testServerPing(server))
|
|
const results = await Promise.all(pingPromises)
|
|
|
|
const updatedServers = results.map((result) => ({
|
|
...result.server,
|
|
ping: result.ping,
|
|
pingStatus: result.status,
|
|
}))
|
|
|
|
setServers(updatedServers)
|
|
} catch (error) {
|
|
console.error("Error testing pings:", error)
|
|
} finally {
|
|
setIsTestingPings(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="bg-gray-900 rounded-lg border border-gray-800 p-6 mb-8">
|
|
<h3 className="text-lg font-bold mb-6">Tunnel Configuration</h3>
|
|
|
|
<div className="mb-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h4 className="text-md font-medium">Choose Your Server Location</h4>
|
|
{servers.length > 0 && hasAutoTested && (
|
|
<button
|
|
onClick={testAllPings}
|
|
disabled={isTestingPings}
|
|
className="text-sm text-emerald-400 hover:text-emerald-300 disabled:text-gray-500 disabled:cursor-not-allowed flex items-center gap-1"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className={isTestingPings ? "animate-spin" : ""}
|
|
>
|
|
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
|
|
<path d="M21 3v5h-5" />
|
|
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
|
|
<path d="M3 21v-5h5" />
|
|
</svg>
|
|
{isTestingPings ? "Testing All..." : "Test All Pings"}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{isLoadingServers ? (
|
|
<div className="bg-gray-800 rounded-lg border border-gray-700 p-8 text-center">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-emerald-400 mx-auto mb-4"></div>
|
|
<p className="text-gray-400">Loading available servers...</p>
|
|
</div>
|
|
) : serverError ? (
|
|
<div className="bg-red-950 rounded-lg border border-red-800 p-6 text-center">
|
|
<div className="mb-4">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="48"
|
|
height="48"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className="text-red-400 mx-auto"
|
|
>
|
|
<circle cx="12" cy="12" r="10" />
|
|
<line x1="15" x2="9" y1="9" y2="15" />
|
|
<line x1="9" x2="15" y1="9" y2="15" />
|
|
</svg>
|
|
</div>
|
|
<p className="text-red-400 font-medium mb-2">Server Unavailable</p>
|
|
<p className="text-red-300 text-sm">{serverError}</p>
|
|
<button
|
|
onClick={() => window.location.reload()}
|
|
className="mt-4 px-4 py-2 bg-red-800 hover:bg-red-700 text-white rounded-lg text-sm transition-colors"
|
|
>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
) : servers.length === 0 ? (
|
|
<div className="bg-yellow-950 rounded-lg border border-yellow-800 p-6 text-center">
|
|
<div className="mb-4">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="48"
|
|
height="48"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className="text-yellow-400 mx-auto"
|
|
>
|
|
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
<line x1="12" x2="12" y1="9" y2="13" />
|
|
<line x1="12" x2="12.01" y1="17" y2="17" />
|
|
</svg>
|
|
</div>
|
|
<p className="text-yellow-400 font-medium mb-2">No Servers Available</p>
|
|
<p className="text-yellow-300 text-sm">All servers are currently offline for maintenance.</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="bg-gray-800 rounded-lg border border-gray-700 p-3 mb-4">
|
|
<ComposableMap
|
|
projection="geoMercator"
|
|
projectionConfig={{
|
|
scale: 100,
|
|
center: [0, 20],
|
|
}}
|
|
width={600}
|
|
height={250}
|
|
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>
|
|
|
|
{servers.map((server) => (
|
|
<Marker
|
|
key={server.id}
|
|
coordinates={server.coordinates}
|
|
onClick={() => {
|
|
if (server.pingStatus === "failed" || server.pingStatus === "timeout") {
|
|
return
|
|
}
|
|
onServerSelect(server)
|
|
}}
|
|
>
|
|
<g>
|
|
{selectedServer?.id === server.id && (
|
|
<circle r="12" fill="none" stroke="#10b981" strokeWidth="2" opacity="0.6">
|
|
<animate attributeName="r" values="6;15;6" dur="2s" repeatCount="indefinite" />
|
|
<animate attributeName="opacity" values="0.6;0;0.6" dur="2s" repeatCount="indefinite" />
|
|
</circle>
|
|
)}
|
|
<circle r="6" fill={getMarkerColor(server)} stroke={getMarkerStroke(server)} strokeWidth="2" />
|
|
<text
|
|
textAnchor="middle"
|
|
y="-12"
|
|
style={{
|
|
fontFamily: "system-ui",
|
|
fontSize: "10px",
|
|
fontWeight: "bold",
|
|
fill: "white",
|
|
pointerEvents: "none",
|
|
}}
|
|
>
|
|
{server.location}
|
|
</text>
|
|
</g>
|
|
</Marker>
|
|
))}
|
|
</ComposableMap>
|
|
</div>
|
|
|
|
<div className="grid gap-3 md:grid-cols-3">
|
|
{servers.map((server) => (
|
|
<div
|
|
key={server.id}
|
|
onClick={() => {
|
|
if (server.pingStatus === "failed" || server.pingStatus === "timeout") {
|
|
return
|
|
}
|
|
onServerSelect(server)
|
|
}}
|
|
className={`p-3 rounded-lg border transition-all duration-200 ${selectedServer?.id === server.id
|
|
? "bg-emerald-950 border-emerald-500"
|
|
: server.pingStatus === "failed" || server.pingStatus === "timeout"
|
|
? "bg-red-950 border-red-800 cursor-not-allowed opacity-75"
|
|
: "bg-gray-800 border-gray-700 hover:border-gray-600 cursor-pointer"
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between mb-1">
|
|
<h5 className="font-medium text-sm">{server.name}</h5>
|
|
<div
|
|
className={`w-2 h-2 rounded-full ${selectedServer?.id === server.id
|
|
? "bg-emerald-400"
|
|
: server.pingStatus === "failed" || server.pingStatus === "timeout"
|
|
? "bg-red-400"
|
|
: server.pingStatus === "success" && server.ping !== null
|
|
? server.ping < 50
|
|
? "bg-green-400"
|
|
: server.ping < 100
|
|
? "bg-yellow-400"
|
|
: server.ping < 150
|
|
? "bg-orange-400"
|
|
: "bg-red-400"
|
|
: "bg-gray-600"
|
|
}`}
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-gray-400 mb-1">{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-xs">Ping:</span>
|
|
<div className="flex items-center gap-1">
|
|
{server.pingStatus === "testing" && (
|
|
<div className="animate-spin rounded-full h-3 w-3 border border-gray-400 border-t-transparent"></div>
|
|
)}
|
|
{hasAutoTested ? (
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
if (server.pingStatus !== "testing") {
|
|
testPingForServer(server)
|
|
}
|
|
}}
|
|
disabled={server.pingStatus === "testing"}
|
|
className={`text-xs font-bold hover:underline disabled:no-underline disabled:cursor-not-allowed ${getPingColor(
|
|
server,
|
|
)}`}
|
|
>
|
|
{getPingDisplay(server)}
|
|
</button>
|
|
) : (
|
|
<span className={`text-xs font-bold ${getPingColor(server)}`}>{getPingDisplay(server)}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<p className="text-xs text-gray-500 mt-1">{getPingStatus(server)}</p>
|
|
{(server.pingStatus === "failed" || server.pingStatus === "timeout") && (
|
|
<p className="text-xs text-red-400 mt-1 font-medium">Cannot select - Server unavailable</p>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{servers.length > 0 && (
|
|
<>
|
|
<div className="mb-6">
|
|
<label className="block text-sm font-medium mb-3">Forwarding Type</label>
|
|
<div className="flex gap-4">
|
|
<label className="flex items-center cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
name="forwardingType"
|
|
value="http"
|
|
checked={localConfig.type === "http"}
|
|
onChange={() => updateConfig({ type: "http", serverPort: 443 })}
|
|
className="sr-only"
|
|
/>
|
|
<div
|
|
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-gray-800 border-gray-700 text-gray-300 hover:border-gray-600"
|
|
}`}
|
|
>
|
|
<div
|
|
className={`w-2 h-2 rounded-full ${localConfig.type === "http" ? "bg-emerald-400" : "bg-gray-500"}`}
|
|
/>
|
|
<span className="font-medium">HTTP/HTTPS</span>
|
|
</div>
|
|
</label>
|
|
|
|
<label className="flex items-center cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
name="forwardingType"
|
|
value="tcp"
|
|
checked={localConfig.type === "tcp"}
|
|
onChange={() => updateConfig({ type: "tcp", serverPort: 8080 })}
|
|
className="sr-only"
|
|
/>
|
|
<div
|
|
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-gray-800 border-gray-700 text-gray-300 hover:border-gray-600"
|
|
}`}
|
|
>
|
|
<div
|
|
className={`w-2 h-2 rounded-full ${localConfig.type === "tcp" ? "bg-emerald-400" : "bg-gray-500"}`}
|
|
/>
|
|
<span className="font-medium">TCP</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
<p className="text-sm text-gray-400 mt-2">
|
|
{localConfig.type === "http"
|
|
? "Best for web applications and APIs. Uses HTTPS (port 443) or HTTP (port 80)."
|
|
: "For any TCP service like databases, game servers, or custom applications."}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid gap-4 md:grid-cols-2 mb-6">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">Server Port (Internet Access)</label>
|
|
{localConfig.type === "http" ? (
|
|
<select
|
|
value={localConfig.serverPort}
|
|
onChange={(e) => updateConfig({ serverPort: Number.parseInt(e.target.value) })}
|
|
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"
|
|
>
|
|
<option value={443}>443 (HTTPS)</option>
|
|
<option value={80}>80 (HTTP)</option>
|
|
</select>
|
|
) : (
|
|
<input
|
|
type="number"
|
|
value={localConfig.serverPort}
|
|
onChange={(e) => updateConfig({ serverPort: Number.parseInt(e.target.value) || 8080 })}
|
|
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"
|
|
placeholder="8080"
|
|
min="1024"
|
|
max="65535"
|
|
/>
|
|
)}
|
|
<p className="text-xs text-gray-400 mt-1">
|
|
{localConfig.type === "http" ? "Standard web ports" : "Port accessible from the internet"}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">Local Port (Your Service)</label>
|
|
<input
|
|
type="number"
|
|
value={localConfig.localPort}
|
|
onChange={(e) => updateConfig({ localPort: Number.parseInt(e.target.value) || 8000 })}
|
|
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"
|
|
placeholder="8000"
|
|
min="1"
|
|
max="65535"
|
|
/>
|
|
<p className="text-xs text-gray-400 mt-1">Port where your local service is running</p>
|
|
</div>
|
|
</div>
|
|
|
|
{selectedServer && (
|
|
<div className="mb-6">
|
|
<label className="block text-sm font-medium mb-2">SSH Command</label>
|
|
<div className="relative">
|
|
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700 font-mono text-sm overflow-x-auto">
|
|
<pre className="whitespace-pre-wrap break-all sm:break-normal">{generateCommand()}</pre>
|
|
</div>
|
|
<button
|
|
onClick={copyToClipboard}
|
|
className="absolute right-3 top-3 h-8 w-8 flex items-center justify-center rounded-md text-gray-400 hover:text-white hover:bg-gray-700 transition-colors"
|
|
aria-label="Copy command"
|
|
>
|
|
{copied ? (
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className="text-emerald-400"
|
|
>
|
|
<path d="M20 6 9 17l-5-5" />
|
|
</svg>
|
|
) : (
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
|
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
</div>
|
|
<p className="text-xs text-gray-400 mt-2">Run this command in your terminal to create the tunnel</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="p-3 bg-gray-800 rounded-lg border border-gray-700">
|
|
<p className="text-sm text-gray-300">
|
|
<span className="font-medium">Traffic Flow:</span> Internet →{" "}
|
|
<span className="text-emerald-400 font-mono">
|
|
{selectedServer ? selectedServer.location : "Server"}:{localConfig.serverPort}
|
|
</span>{" "}
|
|
→ <span className="text-emerald-400 font-mono">localhost:{localConfig.localPort}</span>
|
|
</p>
|
|
<p className="text-xs text-gray-400 mt-1">
|
|
{localConfig.type === "http" ? (
|
|
<>
|
|
Your local service on port {localConfig.localPort} will be accessible via{" "}
|
|
{localConfig.serverPort === 443 ? "HTTPS" : "HTTP"}
|
|
</>
|
|
) : (
|
|
<>
|
|
TCP traffic to server port {localConfig.serverPort} will be forwarded to your localhost:
|
|
{localConfig.localPort}
|
|
</>
|
|
)}
|
|
</p>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export type { Server }
|