commit b005193261f2861d3c59fff08980d843d6b86573 Author: Xi Xu Date: Wed Nov 19 19:30:10 2025 +0800 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. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b3dada0 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +custom: https://xi-xu.me/#sponsorships +buy_me_a_coffee: xixu diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..974b356 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 5 + commit-message: + prefix: "ci" + include: "scope" + labels: + - "dependencies" + - "github-actions" + reviewers: + - "xixu-me" + assignees: + - "xixu-me" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..1ad43e5 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,39 @@ +name: Deploy to Cloudflare Workers + +on: + push: + branches: + - main + paths-ignore: + - "**.md" + - "LICENSE" + - ".gitignore" + - ".editorconfig" + - ".vscode/**" + - "docs/**" + - ".prettierrc*" + - ".eslintrc*" + - ".github/ISSUE_TEMPLATE/**" + - ".github/PULL_REQUEST_TEMPLATE/**" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + deployments: write + steps: + - name: Checkout main branch + uses: actions/checkout@v5 + + - name: Deploy to Cloudflare Workers + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cea50c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Dependencies +node_modules/ +package-lock.json +yarn.lock +pnpm-lock.yaml + +# Wrangler +.wrangler/ +.dev.vars +.mf/ + +# Build outputs +dist/ +worker/ + +# Environment variables +.env +.env.local +.env.production + +# Editor directories +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..99f5cea --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Xi Xu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..df4c7c3 --- /dev/null +++ b/README.md @@ -0,0 +1,161 @@ +# IP Address Lookup + +Fast, secure, and modern IP address geolocation lookup service built with Cloudflare Workers. Provides comprehensive location data, ISP information, timezone detection, and more for IPv4, IPv6 addresses, and domain names via web interface and REST API. + +## Live Demo + +**[https://ip.xi-xu.me](https://ip.xi-xu.me)** + +## Features + +- **Universal Support**: Query IPv4, IPv6 addresses, and domain names +- **Comprehensive Data**: Location, ISP, timezone, coordinates, reverse DNS, and more +- **Modern Web UI**: Beautiful, responsive interface with real-time lookups +- **REST API**: Simple JSON API for programmatic access +- **Edge Computing**: Powered by Cloudflare Workers for global low-latency responses +- **Security Features**: Input validation, CORS support, CSP headers +- **SEO Optimized**: Complete with meta tags, structured data, sitemap, and robots.txt +- **Health Monitoring**: Built-in health check endpoint for uptime monitoring + +## API Reference + +### Endpoints + +#### `GET /api/lookup` + +Look up geolocation data for an IP address or domain name. + +**Query Parameters:** + +- `ip` (optional): IPv4, IPv6 address, or domain name to look up + - If omitted or set to `me`, returns your own IP address information + +**Example Requests:** + +```bash +# Look up your own IP +curl https://ip.xi-xu.me/api/lookup + +# Look up a specific IPv4 address +curl https://ip.xi-xu.me/api/lookup?ip=8.8.8.8 + +# Look up an IPv6 address +curl https://ip.xi-xu.me/api/lookup?ip=2001:4860:4860::8888 + +# Look up a domain name +curl https://ip.xi-xu.me/api/lookup?ip=example.com +``` + +**Example Response:** + +```json +{ + "status": "success", + "continent": "North America", + "continentCode": "NA", + "country": "United States", + "countryCode": "US", + "region": "VA", + "regionName": "Virginia", + "city": "Ashburn", + "district": "", + "zip": "20149", + "lat": 39.03, + "lon": -77.5, + "timezone": "America/New_York", + "offset": -18000, + "currency": "USD", + "isp": "Google LLC", + "org": "Google Public DNS", + "as": "AS15169 Google LLC", + "asname": "GOOGLE", + "reverse": "dns.google", + "mobile": false, + "proxy": false, + "hosting": true, + "query": "8.8.8.8" +} +``` + +**Error Response:** + +```json +{ + "status": "fail", + "message": "Invalid format. Please enter a valid IPv4, IPv6 address, or domain name." +} +``` + +#### `GET /health` + +Health check endpoint for monitoring service availability. + +**Example Request:** + +```bash +curl https://ip.xi-xu.me/health +``` + +**Example Response:** + +```json +{ + "status": "healthy", + "service": "ip-address-lookup", + "timestamp": "2025-01-15T10:30:00.000Z" +} +``` + +### Rate Limits + +This service uses [IP-API.com](https://ip-api.com) for geolocation data, which has the following limits for non-commercial use: + +- **45 requests per minute** from the same IP address +- Requests are cached for 5 minutes to reduce rate limit hits + +If you exceed the rate limit, you'll receive a `429 Too Many Requests` response. + +## Features in Detail + +### Security + +- Input validation using strict regex patterns for IPv4, IPv6, and domain names +- Content Security Policy (CSP) headers +- CORS support for cross-origin requests +- Protection against XSS and injection attacks +- X-Content-Type-Options and X-Frame-Options headers + +### Performance + +- Edge caching with 5-minute TTL for geolocation data +- Smart placement mode for optimal routing +- Minimal JavaScript footprint +- Single-file architecture for fast cold starts +- Pre-cached HTML page to avoid regeneration + +### SEO & Discoverability + +- Comprehensive meta tags (Open Graph, Twitter Cards) +- Schema.org structured data (WebApplication) +- Sitemap.xml for search engine indexing +- Robots.txt with crawl directives +- Semantic HTML with ARIA labels + +### User Experience + +- Automatic lookup of visitor's IP on page load +- Responsive design for mobile and desktop +- Dark theme optimized for readability +- Loading states and error handling +- Keyboard shortcuts (Escape to clear) +- Accessibility features (ARIA labels, screen reader support) + +## License + +Copyright (c) Xi Xu. All rights reserved. + +This repository is licensed under the [MIT License](LICENSE) - see the LICENSE file for details. + +--- + +**Note**: This service is provided for free and is suitable for non-commercial use. Please respect the rate limits and terms of service of the underlying IP-API.com service. diff --git a/package.json b/package.json new file mode 100644 index 0000000..90de816 --- /dev/null +++ b/package.json @@ -0,0 +1,46 @@ +{ + "author": "Xi Xu", + "description": "Fast, secure, and modern IP address geolocation lookup service built with Cloudflare Workers. Provides comprehensive location data, ISP information, timezone detection, and more for IPv4, IPv6 addresses, and domain names via web interface and REST API.", + "devDependencies": { + "wrangler": "^3.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "keywords": [ + "cloudflare", + "workers", + "cloudflare-workers", + "ip-lookup", + "ip-address-lookup", + "geolocation", + "geolocation-api", + "ip-geolocation", + "edge-computing", + "serverless", + "ipv4", + "ipv6", + "dns-lookup", + "reverse-dns", + "isp-lookup", + "ip-location", + "ip-api", + "what-is-my-ip", + "ip-info", + "location-api" + ], + "license": "MIT", + "main": "src/worker.js", + "name": "ip-address-lookup", + "repository": { + "type": "git", + "url": "https://github.com/xixu-me/IP-Address-Lookup" + }, + "scripts": { + "deploy": "wrangler deploy", + "dev": "wrangler dev", + "tail": "wrangler tail", + "test": "wrangler dev --test" + }, + "version": "1.0.0" +} diff --git a/src/worker.js b/src/worker.js new file mode 100644 index 0000000..9d8ca92 --- /dev/null +++ b/src/worker.js @@ -0,0 +1,1163 @@ +// 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. +

+
+ + + +
+
+
+
+ + +
+
+ + + +`; +} diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..8f70040 --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,22 @@ +name = "ip-address-lookup" +main = "src/worker.js" +compatibility_date = "2025-11-18" +workers_dev = false + +[placement] +mode = "smart" + +[observability] +enabled = false +head_sampling_rate = 1 + +[observability.logs] +enabled = true +head_sampling_rate = 1 +persist = true +invocation_logs = true + +[observability.traces] +enabled = true +persist = true +head_sampling_rate = 1