// src/worker.js // IP Address Lookup Service - Cloudflare Worker // Provides geolocation data via web UI and JSON API const IP_API_BASE = "http://ip-api.com/json/"; const IP_API_FIELDS = "status,message,continent,continentCode,country,countryCode,region,regionName,city,district,zip,lat,lon,timezone,offset,currency,isp,org,as,asname,reverse,mobile,proxy,hosting,query"; // Regex patterns for input validation const IPV4_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; const IPV6_REGEX = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; const DOMAIN_REGEX = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/; // Cache the HTML page as a constant to avoid regeneration const CACHED_HTML = renderHtmlPage(); export default { async fetch(request, env, ctx) { const url = new URL(request.url); // Handle CORS preflight if (request.method === "OPTIONS") { return handleOptions(); } // Health check endpoint if (url.pathname === "/health" || url.pathname === "/healthz") { return handleHealthCheck(); } // Robots.txt endpoint if (url.pathname === "/robots.txt") { return handleRobotsTxt(); } // Sitemap.xml endpoint if (url.pathname === "/sitemap.xml") { return handleSitemap(); } if (url.pathname === "/api/lookup") { return handleApiLookup(request, env, ctx); } // Default: serve the HTML UI return handleWebUi(request, env, ctx); }, }; /** * Handles CORS preflight OPTIONS requests */ function handleOptions() { return new Response(null, { status: 204, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": "Content-Type", "Access-Control-Max-Age": "86400", // 24 hours }, }); } /** * Health check endpoint for monitoring */ function handleHealthCheck() { return jsonResponse( { status: "healthy", service: "ip-address-lookup", timestamp: new Date().toISOString(), }, 200 ); } /** * Serves robots.txt for search engine crawlers */ function handleRobotsTxt() { const robotsTxt = `# IP Lookup Worker - robots.txt User-agent: * Allow: / Allow: /api/lookup Disallow: /api/lookup?ip=* # Sitemap Sitemap: https://ip.xi-xu.me/sitemap.xml # Crawl-delay (be nice to our server) Crawl-delay: 1 `; return new Response(robotsTxt, { status: 200, headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "public, max-age=86400", // Cache for 24 hours "X-Content-Type-Options": "nosniff", }, }); } /** * Serves sitemap.xml for search engines */ function handleSitemap() { const sitemap = ` https://ip.xi-xu.me/ ${new Date().toISOString().split("T")[0]} weekly 1.0 https://ip.xi-xu.me/api/lookup ${new Date().toISOString().split("T")[0]} monthly 0.8 https://ip.xi-xu.me/health ${new Date().toISOString().split("T")[0]} monthly 0.3 `; return new Response(sitemap, { status: 200, headers: { "Content-Type": "application/xml; charset=utf-8", "Cache-Control": "public, max-age=86400", // Cache for 24 hours "X-Content-Type-Options": "nosniff", }, }); } /** * Validates input string as IPv4, IPv6, or domain name * @param {string} input - The input to validate * @returns {boolean} - True if valid, false otherwise */ function isValidInput(input) { if (!input || typeof input !== "string") return false; // Trim and check length const trimmed = input.trim(); if (trimmed.length === 0 || trimmed.length > 253) return false; // Check if it matches any valid format return ( IPV4_REGEX.test(trimmed) || IPV6_REGEX.test(trimmed) || DOMAIN_REGEX.test(trimmed) ); } /** * Handles API lookup requests * @param {Request} request - The incoming request * @param {object} env - Environment bindings * @param {object} ctx - Execution context * @returns {Response} JSON response with geolocation data */ async function handleApiLookup(request, env, ctx) { const url = new URL(request.url); // Accept IP from query, otherwise use client IP const ipParam = (url.searchParams.get("ip") || "").trim(); const ipHeader = request.headers.get("CF-Connecting-IP") || request.headers.get("x-real-ip") || ""; let targetIp; if (!ipParam || ipParam.toLowerCase() === "me") { targetIp = ipHeader; } else { targetIp = ipParam; } // Validate that we have an IP if (!targetIp) { return jsonResponse( { status: "fail", message: "Unable to determine your IP address. Please specify an IP address or domain name.", }, 400 ); } // Validate input format to prevent injection attacks if (!isValidInput(targetIp)) { return jsonResponse( { status: "fail", message: "Invalid format. Please enter a valid IPv4, IPv6 address, or domain name.", }, 400 ); } const endpoint = `${IP_API_BASE}${encodeURIComponent( targetIp )}?fields=${encodeURIComponent(IP_API_FIELDS)}`; let upstream; try { upstream = await fetch(endpoint, { // Cloudflare-specific cache hints to reduce rate limit hits cf: { cacheTtl: 300, // 5 minutes cacheEverything: true, }, }); } catch (err) { // Don't leak error details to client console.error("Upstream fetch error:", err); return jsonResponse( { status: "fail", message: "Unable to connect to the geolocation service. Please try again later.", }, 502 ); } // Respect ip-api's rate limiting - return proper 429 status if (upstream.status === 429) { return jsonResponse( { status: "fail", message: "Rate limit exceeded. Please wait one minute before trying again.", }, 429 ); } let data; try { data = await upstream.json(); } catch { return jsonResponse( { status: "fail", message: "Received invalid response from the geolocation service. Please try again.", }, 502 ); } // Determine appropriate status code const statusCode = upstream.ok && data && data.status === "success" ? 200 : 400; return jsonResponse(data, statusCode, { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", }); } /** * Creates a JSON response with appropriate headers * @param {object} body - Response body * @param {number} status - HTTP status code * @param {object} extraHeaders - Additional headers * @returns {Response} JSON response */ function jsonResponse(body, status = 200, extraHeaders = {}) { return new Response(JSON.stringify(body, null, 2), { status, headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "public, max-age=300", "X-Content-Type-Options": "nosniff", ...extraHeaders, }, }); } /** * Serves the HTML web UI * @param {Request} request - The incoming request * @param {object} env - Environment bindings * @param {object} ctx - Execution context * @returns {Response} HTML response */ function handleWebUi(request, env, ctx) { return new Response(CACHED_HTML, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "public, max-age=3600", // Cache for 1 hour "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY", "Referrer-Policy": "strict-origin-when-cross-origin", "Content-Security-Policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; connect-src 'self'; base-uri 'self'; form-action 'self';", }, }); } function renderHtmlPage() { return ` IP Address Lookup - Free Geolocation API & Tool

IP Address Geolocation Lookup Tool

Discover detailed location information for any IPv4, IPv6 address, or domain name. Get instant access to ISP details, timezone, coordinates, and more. Leave the field empty to check your own IP address.

`; }