import { TEST_NODES } from '../shared/types' interface Env { GLOBALPING_API: string } interface MeasurementResponse { id: string probesCount: number } interface ProbeResult { probe: { continent: string country: string city: string asn: number network: string } result: { status: string rawOutput: string stats?: { min: number max: number avg: number total: number loss: number } } } interface MeasurementResult { id: string type: string status: 'in-progress' | 'finished' results?: ProbeResult[] } interface BatchLatencyResult { nodeId: string latency: number | null success: boolean } const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', } function jsonResponse(data: unknown, status = 200): Response { return new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json', ...corsHeaders, }, }) } const IP_REGEX = /^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)$/ const DOMAIN_REGEX = /^(?!:\/\/)([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/ function isValidTarget(target: string): boolean { return IP_REGEX.test(target) || DOMAIN_REGEX.test(target) } function extractClientIp(request: Request): string | null { const cfConnectingIp = request.headers.get('CF-Connecting-IP') if (cfConnectingIp) return cfConnectingIp const forwarded = request.headers.get('X-Forwarded-For') if (forwarded) return forwarded.split(',')[0].trim() return null } async function createBatchMeasurement(target: string, env: Env): Promise { const locations = TEST_NODES.map(node => ({ country: node.country, city: node.city, limit: 1 })) const res = await fetch(`${env.GLOBALPING_API}/measurements`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': 'LatencyTest/1.0.0' }, body: JSON.stringify({ type: 'ping', target, inProgressUpdates: true, locations, measurementOptions: { packets: 3 } }) }) if (!res.ok) { const error = await res.json().catch(() => ({})) as { error?: { message?: string } } throw new Error(error.error?.message || `GlobalPing API error: ${res.status}`) } const data = await res.json() as MeasurementResponse return data.id } function normalizeLocationName(value?: string | null): string { if (!value) return '' return value .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/[^a-zA-Z0-9]+/g, ' ') .trim() .toLowerCase() } function matchProbeToNode(probe: ProbeResult['probe']): string | null { const candidates = TEST_NODES.filter(node => node.country === probe.country) if (candidates.length === 0) return null const probeCity = normalizeLocationName(probe.city) if (probeCity) { for (const node of candidates) { const nodeCity = normalizeLocationName(node.city) if (nodeCity && (probeCity.includes(nodeCity) || nodeCity.includes(probeCity))) { return node.id } } } if (candidates.length === 1) { return candidates[0].id } return null } async function handleGetIp(request: Request): Promise { const ip = extractClientIp(request) if (!ip) { return jsonResponse({ error: 'Unable to determine IP' }, 500) } return jsonResponse({ ip }) } async function handleCreateMeasurement(request: Request, env: Env): Promise { try { const body = await request.json() as { target?: string } const { target } = body if (!target || typeof target !== 'string') { return jsonResponse({ error: 'target is required' }, 400) } const trimmedTarget = target.trim().toLowerCase() if (!isValidTarget(trimmedTarget)) { return jsonResponse({ error: 'Invalid target. Please enter a valid IP address or domain name.' }, 400) } const measurementId = await createBatchMeasurement(trimmedTarget, env) return jsonResponse({ measurementId }) } catch (error) { console.error('Batch measurement creation error:', error) return jsonResponse({ error: 'Failed to create measurement' }, 500) } } async function handleGetMeasurement(measurementId: string, env: Env): Promise { try { const fetchRes = await fetch(`${env.GLOBALPING_API}/measurements/${measurementId}`, { headers: { 'User-Agent': 'LatencyTest/1.0.0' } }) if (!fetchRes.ok) { return jsonResponse({ error: 'Failed to get measurement' }, fetchRes.status) } const data = await fetchRes.json() as MeasurementResult const results: BatchLatencyResult[] = [] const matchedNodes = new Set() if (data.results) { for (const probeResult of data.results) { const result = probeResult.result const nodeId = matchProbeToNode(probeResult.probe) if (nodeId && !matchedNodes.has(nodeId)) { matchedNodes.add(nodeId) if (result.status === 'finished') { const latency = result.stats?.avg != null ? Math.round(result.stats.avg) : null results.push({ nodeId, latency, success: latency !== null }) } else { results.push({ nodeId, latency: null, success: false }) } } } } for (const node of TEST_NODES) { if (!matchedNodes.has(node.id)) { results.push({ nodeId: node.id, latency: null, success: false }) } } return jsonResponse({ status: data.status, results }) } catch (error) { console.error('Get measurement error:', error) return jsonResponse({ error: 'Failed to get measurement' }, 500) } } export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url) const path = url.pathname // Handle CORS preflight if (request.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }) } // API routes if (path === '/api/ip' && request.method === 'GET') { return handleGetIp(request) } if (path === '/api/latency/batch' && request.method === 'POST') { return handleCreateMeasurement(request, env) } const batchMatch = path.match(/^\/api\/latency\/batch\/([a-zA-Z0-9-]+)$/) if (batchMatch && request.method === 'GET') { return handleGetMeasurement(batchMatch[1], env) } // Serve static files for other routes (handled by Cloudflare Pages/Workers Sites) return new Response('Not Found', { status: 404 }) } }