// 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]}weekly1.0https://ip.xi-xu.me/api/lookup${new Date().toISOString().split("T")[0]}monthly0.8https://ip.xi-xu.me/health${new Date().toISOString().split("T")[0]}monthly0.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.