Files
ip-address-lookup/src/worker.js
T
xixu-me b005193261 Initial repo setup for IP Address Lookup
Add Cloudflare Worker source code, configuration files, GitHub Actions workflow, and documentation for the IP Address Lookup service. Includes geolocation lookup worker, deployment workflow, API documentation, and project metadata.
2025-11-19 19:30:10 +08:00

1164 lines
35 KiB
JavaScript

// 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 = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>https://ip.xi-xu.me/</loc>
<lastmod>${new Date().toISOString().split("T")[0]}</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://ip.xi-xu.me/api/lookup</loc>
<lastmod>${new Date().toISOString().split("T")[0]}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://ip.xi-xu.me/health</loc>
<lastmod>${new Date().toISOString().split("T")[0]}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
</urlset>`;
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 `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<!-- Primary Meta Tags -->
<title>IP Address Lookup - Free Geolocation API & Tool</title>
<meta name="title" content="IP Address Lookup - Free Geolocation API & Tool" />
<meta name="description" content="Free IP address geolocation lookup tool and API. Instantly retrieve detailed location data, ISP information, timezone, and more for any IPv4, IPv6 address, or domain name. Fast, secure, and powered by Cloudflare's global edge network." />
<meta name="keywords" content="IP lookup, IP geolocation, IP address lookup, geolocation API, IPv4 lookup, IPv6 lookup, domain lookup, IP location, ISP lookup, what is my IP, free IP API, Cloudflare Workers, IP address tool, geolocation service, reverse DNS, IP to location, find IP location, IP address finder, geolocation lookup, IP info" />
<meta name="author" content="IP Address Lookup" />
<meta name="robots" content="index, follow" />
<meta name="language" content="en" />
<meta name="theme-color" content="#0f172a" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://ip.xi-xu.me/" />
<meta property="og:title" content="IP Address Lookup - Free Geolocation API & Tool" />
<meta property="og:description" content="Discover the location of any IP address or domain with our free geolocation tool. Get comprehensive details including ISP, timezone, coordinates, and proxy detection. Built on Cloudflare's global network for maximum speed and reliability." />
<meta property="og:image" content="https://ip.xi-xu.me/og-image.png" />
<meta property="og:site_name" content="IP Address Lookup" />
<meta property="og:locale" content="en_US" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="https://ip.xi-xu.me/" />
<meta name="twitter:title" content="IP Address Lookup - Free Geolocation API & Tool" />
<meta name="twitter:description" content="Free IP geolocation lookup tool: Find location, ISP, timezone, and more for any IPv4/IPv6 address or domain. Powered by Cloudflare Workers." />
<meta name="twitter:image" content="https://ip.xi-xu.me/twitter-image.png" />
<!-- Canonical URL -->
<link rel="canonical" href="https://ip.xi-xu.me/" />
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌐</text></svg>" />
<!-- Schema.org Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "IP Address Lookup",
"description": "Free IP address geolocation lookup service providing instant location data for IPv4, IPv6 addresses, and domain names. Features include ISP identification, timezone detection, geographic coordinates, reverse DNS lookup, and proxy/VPN detection. Accessible via web interface and comprehensive REST API.",
"url": "https://ip.xi-xu.me/",
"applicationCategory": "UtilityApplication",
"operatingSystem": "Web Browser",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"featureList": [
"IPv4 and IPv6 address geolocation",
"Domain name lookup",
"REST API endpoint",
"ISP and organization information",
"Timezone and currency data",
"Geographic coordinates",
"Reverse DNS lookup",
"Proxy and VPN detection"
],
"browserRequirements": "Requires JavaScript",
"softwareVersion": "2.0.0",
"license": "https://opensource.org/licenses/MIT",
"author": {
"@type": "Organization",
"name": "IP Address Lookup",
"url": "https://github.com/xixu-me/IP-Address-Lookup"
},
"provider": {
"@type": "Organization",
"name": "Cloudflare Workers"
}
}
</script>
<style>
:root {
color-scheme: dark light;
--bg: #0f172a;
--card-bg: #020617;
--fg: #e5e7eb;
--muted: #9ca3af;
--accent: #38bdf8;
--border: #1f2937;
--success: #22c55e;
--error: #ef4444;
--glow-cyan: rgba(56, 189, 248, 0.5);
--glow-green: rgba(34, 197, 94, 0.5);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: radial-gradient(circle at 20% 50%, #1e3a5f 0%, #0f172a 25%, #020617 70%);
background-attachment: fixed;
color: var(--fg);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
overflow-x: hidden;
}
/* Page load animation */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at 20% 50%, rgba(56, 189, 248, 0.03) 0%, transparent 50%);
animation: ambient-pulse 8s ease-in-out infinite;
pointer-events: none;
z-index: 0;
}
main {
width: 100%;
max-width: 960px;
animation: fadeInUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
position: relative;
z-index: 1;
}
.card {
background: rgba(15, 23, 42, 0.95);
border-radius: 1.5rem;
padding: 2rem;
border: 1px solid rgba(148, 163, 184, 0.15);
box-shadow:
0 24px 60px rgba(15, 23, 42, 0.9),
0 0 0 1px rgba(56, 189, 248, 0.05) inset,
0 2px 8px rgba(56, 189, 248, 0.1);
backdrop-filter: blur(24px);
position: relative;
overflow: hidden;
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--accent), var(--success), transparent);
opacity: 0.5;
animation: shimmer 3s ease-in-out infinite;
}
h1 {
font-size: 2rem;
margin: 0 0 0.5rem;
letter-spacing: -0.04em;
font-weight: 700;
background: linear-gradient(135deg, #38bdf8 0%, #22c55e 50%, #38bdf8 100%);
background-size: 200% auto;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: gradient-shift 4s ease infinite;
}
.subtitle {
margin: 0 0 2rem;
color: var(--muted);
font-size: 0.95rem;
line-height: 1.6;
opacity: 0;
animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.2s forwards;
}
form {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-bottom: 1.5rem;
align-items: center;
opacity: 0;
animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.3s forwards;
}
label {
font-size: 0.875rem;
color: var(--muted);
flex-basis: 100%;
font-weight: 500;
}
.input-wrapper {
display: flex;
gap: 0.75rem;
flex: 1 1 100%;
flex-wrap: wrap;
}
input[type="text"] {
flex: 1 1 auto;
min-width: 200px;
padding: 0.875rem 1.125rem;
border-radius: 0.875rem;
border: 1px solid var(--border);
background: rgba(15, 23, 42, 0.9);
color: var(--fg);
outline: none;
font-size: 0.95rem;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
position: relative;
}
input[type="text"]:focus {
border-color: var(--accent);
box-shadow:
0 0 0 3px rgba(56, 189, 248, 0.15),
0 4px 12px rgba(56, 189, 248, 0.2);
transform: translateY(-1px);
}
input[type="text"]::placeholder {
color: #64748b;
transition: color 0.3s ease;
}
input[type="text"]:focus::placeholder {
color: #94a3b8;
}
button {
padding: 0.875rem 1.75rem;
border-radius: 0.875rem;
border: none;
background: linear-gradient(135deg, #22c55e 0%, #38bdf8 100%);
background-size: 200% auto;
color: #020617;
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
white-space: nowrap;
position: relative;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.25);
}
button::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.6s ease, height 0.6s ease;
}
button:hover:not(:disabled) {
background-position: right center;
box-shadow:
0 8px 24px rgba(56, 189, 248, 0.4),
0 0 24px rgba(34, 197, 94, 0.3);
transform: translateY(-2px);
}
button:active:not(:disabled)::before {
width: 300px;
height: 300px;
transition: width 0s, height 0s;
}
button:disabled {
opacity: 0.7;
cursor: not-allowed;
animation: pulse 2s ease-in-out infinite;
}
.results-wrapper {
display: grid;
gap: 1.25rem;
opacity: 0;
animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.4s forwards;
}
.pill-row {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.pill {
padding: 0.5rem 1rem;
border-radius: 999px;
background: rgba(15, 118, 110, 0.25);
font-size: 0.8rem;
color: #a5f3fc;
border: 1px solid rgba(45, 212, 191, 0.4);
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
animation: slideInDown 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
backdrop-filter: blur(8px);
}
.pill:hover {
background: rgba(15, 118, 110, 0.4);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(45, 212, 191, 0.3);
}
.pill.error {
background: rgba(220, 38, 38, 0.25);
border-color: rgba(248, 113, 113, 0.55);
color: #fecaca;
}
.pill.meta {
background: rgba(30, 64, 175, 0.25);
border-color: rgba(96, 165, 250, 0.5);
color: #bfdbfe;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 1rem;
}
.field {
padding: 1rem 1.125rem;
border-radius: 1.125rem;
background: rgba(15, 23, 42, 0.9);
border: 1px solid rgba(31, 41, 55, 0.9);
position: relative;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
animation: scaleIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
opacity: 0;
transform: scale(0.95);
}
.field:nth-child(1) { animation-delay: 0.05s; }
.field:nth-child(2) { animation-delay: 0.1s; }
.field:nth-child(3) { animation-delay: 0.15s; }
.field:nth-child(4) { animation-delay: 0.2s; }
.field:nth-child(5) { animation-delay: 0.25s; }
.field:nth-child(6) { animation-delay: 0.3s; }
.field:nth-child(7) { animation-delay: 0.35s; }
.field:nth-child(8) { animation-delay: 0.4s; }
.field:nth-child(9) { animation-delay: 0.45s; }
.field:nth-child(10) { animation-delay: 0.5s; }
.field:hover {
border-color: rgba(56, 189, 248, 0.4);
background: rgba(15, 23, 42, 1);
box-shadow: 0 4px 16px rgba(56, 189, 248, 0.15);
}
.field::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 1.125rem;
background: linear-gradient(135deg, rgba(56, 189, 248, 0.1), transparent);
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.field:hover::before {
opacity: 1;
}
.field-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.field-label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
font-weight: 600;
}
.field-icon {
width: 14px;
height: 14px;
opacity: 0.5;
transition: all 0.3s ease;
}
.field:hover .field-icon {
opacity: 0.8;
transform: scale(1.1);
}
.field-value {
font-size: 0.95rem;
word-break: break-word;
display: flex;
align-items: center;
gap: 0.5rem;
justify-content: space-between;
}
.field-value strong {
font-weight: 600;
color: var(--accent);
}
.skeleton {
background: linear-gradient(
90deg,
rgba(31, 41, 55, 0.6) 0%,
rgba(55, 65, 81, 0.8) 50%,
rgba(31, 41, 55, 0.6) 100%
);
background-size: 200% 100%;
border-radius: 0.5rem;
height: 1rem;
animation: shimmer 1.5s ease-in-out infinite;
}
.skeleton-field {
padding: 1rem 1.125rem;
border-radius: 1.125rem;
background: rgba(15, 23, 42, 0.9);
border: 1px solid rgba(31, 41, 55, 0.9);
animation: pulse 1.5s ease-in-out infinite;
}
.footer {
margin-top: 1.5rem;
padding-top: 1.25rem;
border-top: 1px solid rgba(148, 163, 184, 0.1);
font-size: 0.8rem;
color: var(--muted);
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5rem;
opacity: 0;
animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.5s forwards;
}
.footer a {
transition: all 0.2s ease;
}
.footer a:hover {
color: var(--accent);
text-shadow: 0 0 8px var(--glow-cyan);
}
.footer code {
background: rgba(56, 189, 248, 0.1);
padding: 0.2rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.75rem;
color: var(--accent);
border: 1px solid rgba(56, 189, 248, 0.2);
transition: all 0.2s ease;
}
.footer code:hover {
background: rgba(56, 189, 248, 0.2);
border-color: rgba(56, 189, 248, 0.4);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes slideInDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
@keyframes gradient-shift {
0%, 100% {
background-position: 0% center;
}
50% {
background-position: 100% center;
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
@keyframes ambient-pulse {
0%, 100% {
opacity: 0.3;
}
50% {
opacity: 0.6;
}
}
@media (max-width: 640px) {
.card {
padding: 1.5rem;
border-radius: 1.25rem;
}
h1 {
font-size: 1.5rem;
}
.input-wrapper {
flex-direction: column;
}
input[type="text"] {
width: 100%;
}
button {
width: 100%;
}
.grid {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>
</head>
<body>
<main>
<article class="card">
<header>
<h1>IP Address Geolocation Lookup Tool</h1>
<p class="subtitle">
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.
</p>
</header>
<form id="lookup-form" role="search" aria-label="IP address lookup">
<label for="ip-input">IP address or domain name</label>
<div class="input-wrapper">
<input
id="ip-input"
type="text"
name="ip"
placeholder="Enter IP address or domain (e.g., 8.8.8.8 or example.com) or leave blank"
autocomplete="off"
aria-describedby="input-help"
maxlength="253"
/>
<button type="submit" id="lookup-btn" aria-live="polite">
<span id="btn-text">Look Up</span>
</button>
</div>
<div id="input-help" class="sr-only">Enter an IPv4, IPv6 address, or domain name. Leave blank to look up your own IP address.</div>
</form>
<div class="results-wrapper">
<div class="pill-row" id="pill-row" role="status" aria-live="polite"></div>
<div class="grid" id="results-grid" role="region" aria-label="Lookup results"></div>
</div>
<div class="footer">
<span>Geolocation data provided by <a href="https://ip-api.com" target="_blank" rel="noopener noreferrer" style="color: var(--accent); text-decoration: none;">IP-API.com</a> · Free for non-commercial use · Rate limit: 45 requests per minute</span>
<span>API endpoint: <code>/api/lookup?ip=&lt;address&gt;</code> · Health check: <code>/health</code></span>
<span style="display: flex; align-items: center; justify-content: center; gap: 0.3rem;">
<a href="https://github.com/xixu-me/IP-Address-Lookup" target="_blank" rel="noopener noreferrer" style="color: var(--accent); text-decoration: none; display: inline-flex; align-items: center; gap: 0.3rem; transition: all 0.2s ease;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
Open Source
</a>
<span>·</span>
<a href="https://github.com/xixu-me/IP-Address-Lookup/blob/main/LICENSE" target="_blank" rel="noopener noreferrer" style="color: var(--accent); text-decoration: none;">MIT License</a>
</span>
</div>
</article>
</main>
<script>
const form = document.getElementById('lookup-form');
const ipInput = document.getElementById('ip-input');
const lookupBtn = document.getElementById('lookup-btn');
const btnText = document.getElementById('btn-text');
const pillRow = document.getElementById('pill-row');
const grid = document.getElementById('results-grid');
// SVG Icons
const icons = {
location: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>',
globe: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>',
network: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path></svg>',
server: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>'
};
function setLoading(isLoading) {
lookupBtn.disabled = isLoading;
btnText.textContent = isLoading ? 'Looking up...' : 'Look Up';
}
function clearResults() {
pillRow.innerHTML = '';
grid.innerHTML = '';
}
function showSkeletons() {
grid.innerHTML = Array(20).fill(0).map(() =>
'<div class="skeleton-field"><div class="skeleton"></div></div>'
).join('');
}
function addPill(text, type) {
const span = document.createElement('span');
span.className = 'pill' + (type ? ' ' + type : '');
span.textContent = text;
pillRow.appendChild(span);
}
function getIconForField(label) {
if (['City', 'Location', 'Region', 'Country', 'Country Code', 'Continent', 'Continent Code', 'District', 'ZIP / Postal'].includes(label)) return icons.location;
if (['Latitude', 'Longitude', 'Timezone', 'Offset', 'Currency'].includes(label)) return icons.globe;
if (['ISP', 'Organization', 'AS', 'AS Name'].includes(label)) return icons.network;
if (['IP', 'Reverse DNS', 'Mobile', 'Proxy / VPN / Tor', 'Hosting / Datacenter'].includes(label)) return icons.server;
return icons.network;
}
function addField(label, value) {
if (value === undefined || value === null || value === '') return;
const wrapper = document.createElement('div');
wrapper.className = 'field';
const header = document.createElement('div');
header.className = 'field-header';
const labelEl = document.createElement('div');
labelEl.className = 'field-label';
labelEl.textContent = label;
const iconEl = document.createElement('div');
iconEl.className = 'field-icon';
iconEl.innerHTML = getIconForField(label);
header.appendChild(labelEl);
header.appendChild(iconEl);
const valueEl = document.createElement('div');
valueEl.className = 'field-value';
const textSpan = document.createElement('span');
textSpan.textContent = value;
valueEl.appendChild(textSpan);
wrapper.appendChild(header);
wrapper.appendChild(valueEl);
grid.appendChild(wrapper);
}
// Client-side input validation
function validateInput(input) {
if (!input) return true; // Empty is OK (means "me")
const trimmed = input.trim();
if (trimmed.length > 253) return false;
// Basic validation patterns
const ipv4 = /^(?:(?: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 = /^[0-9a-fA-F:]+$/; // Simplified for client-side
const domain = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/;
return ipv4.test(trimmed) || ipv6.test(trimmed) || domain.test(trimmed);
}
async function performLookup(ip) {
clearResults();
// Client-side validation
if (ip && !validateInput(ip)) {
addPill('Invalid format. Please enter a valid IPv4, IPv6 address, or domain name.', 'error');
return;
}
setLoading(true);
showSkeletons();
try {
const qs = ip ? '?ip=' + encodeURIComponent(ip) : '';
const res = await fetch('/api/lookup' + qs);
const data = await res.json();
grid.innerHTML = '';
if (!res.ok || data.status !== 'success') {
addPill('Lookup failed' + (data.message ? ': ' + data.message : '. Please try again.'), 'error');
return;
}
addPill('✓ Lookup completed successfully', 'meta');
const loc = [data.city, data.district, data.regionName, data.country].filter(Boolean).join(', ');
addField('IP', data.query || '');
addField('Location', loc || '');
addField('Continent', data.continent || '');
addField('Continent Code', data.continentCode || '');
addField('Country', data.country || '');
addField('Country Code', data.countryCode || '');
addField('Region', data.regionName || data.region || '');
addField('City', data.city || '');
addField('District', data.district || '');
addField('ZIP / Postal', data.zip || '');
addField('Latitude', data.lat != null ? String(data.lat) : '');
addField('Longitude', data.lon != null ? String(data.lon) : '');
addField('Timezone', data.timezone || '');
addField('Offset', data.offset != null ? String(data.offset) + ' seconds' : '');
addField('Currency', data.currency || '');
addField('ISP', data.isp || '');
addField('Organization', data.org || '');
addField('AS', data.as || '');
addField('AS Name', data.asname || '');
addField('Reverse DNS', data.reverse || '');
addField('Mobile', data.mobile ? '✓ Yes' : '✗ No');
addField('Proxy / VPN / Tor', data.proxy ? '⚠ Likely' : '✓ Unlikely');
addField('Hosting / Datacenter', data.hosting ? '⚠ Likely' : '✓ Unlikely');
} catch (err) {
grid.innerHTML = '';
addPill('Network error. Please check your connection and try again.', 'error');
} finally {
setLoading(false);
}
}
form.addEventListener('submit', (e) => {
e.preventDefault();
performLookup(ipInput.value.trim());
});
// Automatically look up visitor's IP address on page load
window.addEventListener('load', () => {
// Small delay to allow page to fully render
setTimeout(() => {
performLookup('');
}, 100);
});
// Add keyboard shortcut: Escape to clear input
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && document.activeElement === ipInput) {
ipInput.value = '';
ipInput.blur();
}
});
</script>
</body>
</html>`;
}