57 | Examples: yahoo.com, 8.8.8.8, 2001:4860:4860::8888, AS13335 58 |
59 |${JSON.stringify(data.data, null, 2)}`; 438 | } 439 | 440 | // Add event listener to IP lookup buttons 441 | const ipButtons = document.querySelectorAll('.ipLookupButton'); 442 | ipButtons.forEach(button => { 443 | button.addEventListener('click', () => handleIpLookupClick(button.textContent.trim())); 444 | }); 445 | 446 | // Show print button after successful lookup 447 | printButton.classList.remove('hidden'); 448 | 449 | toastManager.show('Lookup complete', 'success', false, 1000); 450 | } 451 | } catch (error) { 452 | console.error(error); 453 | toastManager.show('Error performing lookup. Please try again.', 'error', true); 454 | } 455 | } 456 | 457 | function printResults() { 458 | const title = queryInput.value.trim(); 459 | const originalTitle = document.title; 460 | document.title = `DumbWhois - ${title}`; 461 | window.print(); 462 | document.title = originalTitle; 463 | } 464 | 465 | // Check for lookup query parameter and hash on page load 466 | const performLookupOnLoad = async () => { 467 | const urlParams = new URLSearchParams(window.location.search); 468 | const lookupQuery = urlParams.get('lookup'); 469 | if (lookupQuery) { 470 | queryInput.value = lookupQuery; 471 | performLookup().then(() => { 472 | // After lookup completes, check if there's a hash to scroll to 473 | if (window.location.hash) { 474 | // Small delay to ensure content is rendered 475 | setTimeout(() => { 476 | const targetElement = document.querySelector(window.location.hash); 477 | if (targetElement) { 478 | targetElement.scrollIntoView(); 479 | } 480 | }, 500); 481 | } 482 | }); 483 | } 484 | } 485 | 486 | const addButtonEventListeners = () => { 487 | lookupButton.addEventListener('click', performLookup); 488 | themeToggleBtn.addEventListener('click', toggleDarkMode); themeToggleBtn.addEventListener('click', toggleDarkMode); 489 | queryInput.addEventListener('keypress', (e) => { 490 | if (e.key === 'Enter') { 491 | performLookup(); 492 | } 493 | }); 494 | printButton.addEventListener('click', printResults); 495 | } 496 | 497 | const initialize = async () => { 498 | // Check local storage, default to dark mode if not set 499 | if (localStorage.getItem('darkMode') === 'false') { 500 | document.documentElement.classList.remove('dark'); 501 | } 502 | 503 | // Initialize App 504 | addButtonEventListeners(); 505 | updateThemeIcon(); 506 | setSiteTitle(); 507 | performLookupOnLoad(); 508 | 509 | queryInput.focus(); 510 | } 511 | 512 | initialize(); 513 | }); -------------------------------------------------------------------------------- /public/managers/toast.js: -------------------------------------------------------------------------------- 1 | export class ToastManager { 2 | constructor(containerElement) { 3 | this.container = containerElement; 4 | this.isError = 'error'; 5 | this.isSuccess = 'success'; 6 | } 7 | 8 | show(message, type = 'success', isStatic = false, timeoutMs = 2000) { 9 | const toast = document.createElement('div'); 10 | toast.classList.add('toast'); 11 | toast.textContent = message; 12 | 13 | if (type === this.isSuccess) toast.classList.add('success'); 14 | else toast.classList.add('error'); 15 | 16 | this.container.appendChild(toast); 17 | 18 | setTimeout(() => { 19 | toast.addEventListener('click', () => this.hide(toast)); 20 | toast.classList.add('show'); 21 | }, 10); 22 | 23 | if (!isStatic) { 24 | setTimeout(() => { 25 | toast.classList.remove('show'); 26 | setTimeout(() => { 27 | this.hide(toast); 28 | }, 300); // Match transition duration 29 | }, timeoutMs); 30 | } 31 | } 32 | 33 | hide(toast) { 34 | toast.classList.remove('show'); 35 | setTimeout(() => { 36 | this.container.removeChild(toast); 37 | }, 300); 38 | } 39 | 40 | clear() { 41 | // use to clear static toast messages 42 | while (this.container.firstChild) { 43 | this.container.removeChild(this.container.firstChild); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /public/service-worker.js: -------------------------------------------------------------------------------- 1 | const CACHE_NAME = "DUMBWHOIS_PWA_CACHE_V1"; 2 | const ASSETS_TO_CACHE = []; 3 | 4 | const preload = async () => { 5 | console.log("Installing web app"); 6 | return await caches.open(CACHE_NAME) 7 | .then(async (cache) => { 8 | console.log("caching index and important routes"); 9 | const response = await fetch("/asset-manifest.json"); 10 | const assets = await response.json(); 11 | ASSETS_TO_CACHE.push(...assets); 12 | console.log("Assets Cached:", ASSETS_TO_CACHE); 13 | return cache.addAll(ASSETS_TO_CACHE); 14 | }); 15 | } 16 | 17 | // Fetch asset manifest dynamically 18 | globalThis.addEventListener("install", (event) => { 19 | event.waitUntil(preload()); 20 | }); 21 | 22 | globalThis.addEventListener("activate", (event) => { 23 | event.waitUntil(clients.claim()); 24 | }); 25 | 26 | globalThis.addEventListener("fetch", (event) => { 27 | event.respondWith( 28 | caches.match(event.request).then((cachedResponse) => { 29 | return cachedResponse || fetch(event.request); 30 | }) 31 | ); 32 | }); -------------------------------------------------------------------------------- /scripts/cors.js: -------------------------------------------------------------------------------- 1 | const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS || '*'; 2 | const NODE_ENV = process.env.NODE_ENV || 'production'; 3 | let allowedOrigins = []; 4 | 5 | function setupOrigins() { 6 | if (NODE_ENV === 'development' || ALLOWED_ORIGINS === '*') allowedOrigins = '*'; 7 | else if (ALLOWED_ORIGINS && typeof ALLOWED_ORIGINS === 'string') { 8 | try { 9 | const allowed = ALLOWED_ORIGINS.split(',').map(origin => origin.trim()); 10 | allowed.forEach(origin => { 11 | const normalizedOrigin = normalizeOrigin(origin); 12 | allowedOrigins.push(normalizedOrigin); 13 | }); 14 | } 15 | catch (error) { 16 | console.error(`Error setting up ALLOWED_ORIGINS: ${ALLOWED_ORIGINS}:`, error); 17 | } 18 | } 19 | console.log("ALLOWED ORIGINS:", allowedOrigins); 20 | return allowedOrigins; 21 | } 22 | 23 | function normalizeOrigin(origin) { 24 | if (origin) { 25 | try { 26 | const normalizedOrigin = new URL(origin).origin; 27 | return normalizedOrigin; 28 | } catch (error) { 29 | console.error("Error parsing referer URL:", error); 30 | throw new Error("Error parsing referer URL:", error); 31 | } 32 | } 33 | } 34 | 35 | function validateOrigin(origin) { 36 | if (NODE_ENV === 'development' || allowedOrigins === '*') return true; 37 | 38 | try { 39 | if (origin) origin = normalizeOrigin(origin); 40 | else { 41 | console.warn("No origin to validate."); 42 | return false; 43 | } 44 | 45 | console.log("Validating Origin:", origin); 46 | 47 | if (allowedOrigins.includes(origin)) { 48 | console.log("Allowed request from origin:", origin); 49 | return true; 50 | } 51 | else { 52 | console.warn("Blocked request from origin:", origin); 53 | return false; 54 | } 55 | } 56 | catch (error) { 57 | console.error(error); 58 | } 59 | } 60 | 61 | function originValidationMiddleware(req, res, next) { 62 | const origin = req.headers.referer || `${req.protocol}://${req.headers.host}`; 63 | const isOriginValid = validateOrigin(origin); 64 | 65 | if (isOriginValid) { 66 | next(); 67 | } else { 68 | res.status(403).json({ error: 'Forbidden' }); 69 | } 70 | } 71 | 72 | 73 | function getCorsOptions() { 74 | const allowedOrigins = setupOrigins(); 75 | const corsOptions = { 76 | origin: allowedOrigins, 77 | credentials: true, 78 | methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], 79 | allowedHeaders: ['Content-Type', 'Authorization'], 80 | }; 81 | 82 | return corsOptions; 83 | } 84 | 85 | module.exports = { getCorsOptions, originValidationMiddleware }; -------------------------------------------------------------------------------- /scripts/pwa-manifest-generator.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const PUBLIC_DIR = path.join(__dirname, "..", "public"); 4 | const ASSETS_DIR = path.join(PUBLIC_DIR, "assets"); 5 | 6 | function getFiles(dir, basePath = "/") { 7 | let fileList = []; 8 | const files = fs.readdirSync(dir); 9 | 10 | files.forEach((file) => { 11 | const filePath = path.join(dir, file); 12 | const fileUrl = path.join(basePath, file).replace(/\\/g, "/"); 13 | 14 | if (fs.statSync(filePath).isDirectory()) { 15 | fileList = fileList.concat(getFiles(filePath, fileUrl)); 16 | } else { 17 | fileList.push(fileUrl); 18 | } 19 | }); 20 | 21 | return fileList; 22 | } 23 | 24 | function generateAssetManifest() { 25 | const assets = getFiles(PUBLIC_DIR); 26 | fs.writeFileSync(path.join(ASSETS_DIR, "asset-manifest.json"), JSON.stringify(assets, null, 2)); 27 | console.log("Asset manifest generated!", assets); 28 | } 29 | 30 | function generatePWAManifest(siteTitle) { 31 | generateAssetManifest(); // fetched later in service-worker 32 | 33 | const pwaManifest = { 34 | name: siteTitle, 35 | short_name: siteTitle, 36 | description: "A simple WHOIS lookup web application using free APIs", 37 | start_url: "/", 38 | display: "standalone", 39 | background_color: "#ffffff", 40 | theme_color: "#000000", 41 | icons: [ 42 | { 43 | src: "assets/logo.png", 44 | type: "image/png", 45 | sizes: "192x192" 46 | }, 47 | { 48 | src: "assets/logo.png", 49 | type: "image/png", 50 | sizes: "512x512" 51 | } 52 | ], 53 | orientation: "any" 54 | }; 55 | 56 | fs.writeFileSync(path.join(ASSETS_DIR, "manifest.json"), JSON.stringify(pwaManifest, null, 2)); 57 | console.log("PWA manifest generated!", pwaManifest); 58 | } 59 | 60 | module.exports = { generatePWAManifest }; -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const cors = require('cors'); 4 | const axios = require('axios'); 5 | const path = require('path'); 6 | const whois = require('node-whois'); 7 | const util = require('util'); 8 | const { getCorsOptions, originValidationMiddleware } = require('./scripts/cors'); 9 | const { generatePWAManifest } = require('./scripts/pwa-manifest-generator'); 10 | 11 | const app = express(); 12 | const PORT = process.env.PORT || 3000; 13 | const SITE_TITLE = process.env.SITE_TITLE || 'DumbWhois'; 14 | const PUBLIC_DIR = path.join(__dirname, 'public'); 15 | const ASSETS_DIR = path.join(PUBLIC_DIR, 'assets'); 16 | 17 | // Convert whois.lookup to Promise 18 | const lookupPromise = util.promisify(whois.lookup); 19 | 20 | // Trust proxy - required for secure cookies behind a reverse proxy 21 | app.set('trust proxy', 1); 22 | 23 | // CORS setup 24 | const corsOptions = getCorsOptions(); 25 | app.use(cors(corsOptions)); 26 | app.use(express.json()); 27 | app.use(originValidationMiddleware); 28 | 29 | generatePWAManifest(SITE_TITLE); 30 | 31 | app.use(express.static('public')); 32 | 33 | // Helper function to detect query type 34 | function detectQueryType(query) { 35 | // Clean up the query - remove brackets if present 36 | const cleanQuery = query.replace(/^\[|\]$/g, ''); 37 | 38 | // ASN pattern (AS followed by numbers) 39 | if (/^(AS|as)?\d+$/i.test(cleanQuery)) { 40 | return 'asn'; 41 | } 42 | 43 | // IPv6 pattern (with optional CIDR) 44 | if (/^(?:(?:[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,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,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,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(?:\/\d{1,3})?$/.test(cleanQuery)) { 45 | return 'ip'; 46 | } 47 | 48 | // IPv4 pattern (with optional CIDR) 49 | if (/^(?:(?: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]?)(?:\/\d{1,2})?$/.test(cleanQuery)) { 50 | return 'ip'; 51 | } 52 | 53 | // Domain pattern (anything with a dot that's not an IP) 54 | if (cleanQuery.includes('.')) { 55 | return 'whois'; 56 | } 57 | return 'unknown'; 58 | } 59 | 60 | // Helper function to parse WHOIS data 61 | async function parseWhoisData(data, domain) { 62 | // Split into lines and create key-value pairs 63 | const result = { 64 | domainName: domain, 65 | registrar: '', 66 | creationDate: '', 67 | expirationDate: '', 68 | lastUpdated: '', 69 | status: [], 70 | nameservers: [], 71 | ipAddresses: { 72 | v4: [], 73 | v6: [] 74 | }, 75 | raw: data 76 | }; 77 | 78 | // Get both IPv4 and IPv6 addresses from DNS lookup 79 | try { 80 | const dns = require('dns').promises; 81 | const [ipv4Addresses, ipv6Addresses] = await Promise.all([ 82 | dns.resolve4(domain).catch(() => []), 83 | dns.resolve6(domain).catch(() => []) 84 | ]); 85 | result.ipAddresses.v4 = ipv4Addresses; 86 | result.ipAddresses.v6 = ipv6Addresses; 87 | } catch (e) { 88 | // If DNS lookup fails, keep arrays empty 89 | } 90 | 91 | // Regular expressions for IP addresses 92 | const ipv4Regex = /\b(?:(?: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]?)\b/g; 93 | const ipv6Regex = /(?:[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,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,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])/g; 94 | 95 | // First try to find IPs in specific fields that might contain them 96 | const lines = data.split('\n'); 97 | for (const line of lines) { 98 | const trimmedLine = line.trim().toLowerCase(); 99 | if (trimmedLine.includes('ip address') || 100 | trimmedLine.includes('a record') || 101 | trimmedLine.includes('aaaa record') || 102 | trimmedLine.includes('addresses') || 103 | trimmedLine.includes('host') || 104 | trimmedLine.includes('dns')) { 105 | 106 | const ipv4InLine = line.match(ipv4Regex); 107 | const ipv6InLine = line.match(ipv6Regex); 108 | 109 | if (ipv4InLine) result.ipAddresses.v4.push(...ipv4InLine); 110 | if (ipv6InLine) result.ipAddresses.v6.push(...ipv6InLine); 111 | } 112 | } 113 | 114 | // Remove duplicates 115 | result.ipAddresses.v4 = [...new Set(result.ipAddresses.v4)]; 116 | result.ipAddresses.v6 = [...new Set(result.ipAddresses.v6)]; 117 | 118 | // Special handling for .eu domains 119 | if (domain.toLowerCase().endsWith('.eu')) { 120 | const lines = data.split('\n'); 121 | let currentSection = ''; 122 | 123 | for (const line of lines) { 124 | const trimmedLine = line.trim(); 125 | 126 | // Skip empty lines and comment lines 127 | if (!trimmedLine || trimmedLine.startsWith('%')) continue; 128 | 129 | // Check for section headers 130 | if (trimmedLine.endsWith(':')) { 131 | currentSection = trimmedLine.slice(0, -1).toLowerCase(); 132 | continue; 133 | } 134 | 135 | // Handle indented lines (section content) 136 | if (line.startsWith(' ')) { 137 | const [key, ...values] = line.trim().split(':').map(s => s.trim()); 138 | const value = values.join(':').trim(); 139 | 140 | switch (currentSection) { 141 | case 'registrar': 142 | if (key === 'Name') { 143 | result.registrar = value; 144 | } 145 | break; 146 | case 'name servers': 147 | if (!key.includes(':') && key !== 'Please visit www.eurid.eu for more info.') { 148 | result.nameservers.push(key); 149 | } 150 | break; 151 | case 'technical': 152 | if (key === 'Organisation' && !result.registrar) { 153 | result.registrar = value; 154 | } 155 | break; 156 | } 157 | } else if (line.includes(':')) { 158 | const [key, ...values] = line.split(':').map(s => s.trim()); 159 | const value = values.join(':').trim(); 160 | 161 | if (key === 'Domain') { 162 | result.domainName = value; 163 | } 164 | } 165 | } 166 | 167 | // Add default status for .eu domains if none found 168 | if (result.status.length === 0) { 169 | result.status.push('registered'); 170 | } 171 | } else { 172 | // Original parsing logic for non-.eu domains 173 | const lines = data.split('\n'); 174 | for (const line of lines) { 175 | const [key, ...values] = line.split(':').map(s => s.trim()); 176 | const value = values.join(':').trim(); 177 | 178 | if (!key || !value) continue; 179 | 180 | const keyLower = key.toLowerCase(); 181 | 182 | // Registrar information 183 | if (keyLower.includes('registrar')) { 184 | result.registrar = value; 185 | } 186 | // Creation date 187 | else if (keyLower.includes('creation') || keyLower.includes('created') || 188 | keyLower.includes('registered')) { 189 | result.creationDate = value; 190 | } 191 | // Expiration date 192 | else if (keyLower.includes('expir')) { 193 | result.expirationDate = value; 194 | } 195 | // Last updated 196 | else if (keyLower.includes('updated') || keyLower.includes('modified')) { 197 | result.lastUpdated = value; 198 | } 199 | // Status 200 | else if (keyLower.includes('status')) { 201 | const statuses = value.split(/[,;]/).map(s => s.trim()); 202 | result.status.push(...statuses); 203 | } 204 | // Nameservers 205 | else if (keyLower.includes('name server') || keyLower.includes('nameserver')) { 206 | const ns = value.split(/[\s,;]+/)[0]; 207 | if (ns && !result.nameservers.includes(ns)) { 208 | result.nameservers.push(ns); 209 | } 210 | } 211 | } 212 | } 213 | 214 | return result; 215 | } 216 | 217 | // IP lookup services with fallbacks 218 | const ipLookupServices = [ 219 | { 220 | name: 'ipapi.co', 221 | url: (ip) => `https://ipapi.co/${ip}/json/`, 222 | transform: (data) => ({ 223 | ...data, 224 | source: 'ipapi.co' 225 | }) 226 | }, 227 | { 228 | name: 'ip-api.com', 229 | url: (ip) => `http://ip-api.com/json/${ip}`, 230 | transform: (data) => ({ 231 | ip: data.query, 232 | version: data.query.includes(':') ? 'IPv6' : 'IPv4', 233 | city: data.city, 234 | region: data.regionName, 235 | region_code: data.region, 236 | country_code: data.countryCode, 237 | country_name: data.country, 238 | postal: data.zip, 239 | latitude: data.lat, 240 | longitude: data.lon, 241 | timezone: data.timezone, 242 | org: data.org || data.isp, 243 | asn: data.as, 244 | source: 'ip-api.com' 245 | }) 246 | }, 247 | { 248 | name: 'ipwho.is', 249 | url: (ip) => `https://ipwho.is/${ip}`, 250 | transform: (data) => ({ 251 | ip: data.ip, 252 | version: data.type, 253 | city: data.city, 254 | region: data.region, 255 | region_code: data.region_code, 256 | country_code: data.country_code, 257 | country_name: data.country, 258 | postal: data.postal, 259 | latitude: data.latitude, 260 | longitude: data.longitude, 261 | timezone: data.timezone.id, 262 | org: data.connection.org, 263 | asn: data.connection.asn, 264 | source: 'ipwho.is' 265 | }) 266 | } 267 | ]; 268 | 269 | // Helper function to try IP lookup services in sequence 270 | async function tryIpLookup(ip) { 271 | // Remove brackets and CIDR notation for the lookup 272 | const cleanIp = ip.replace(/^\[|\]$/g, '').replace(/\/\d+$/, ''); 273 | let lastError = null; 274 | 275 | for (const service of ipLookupServices) { 276 | try { 277 | console.log(`Trying IP lookup with ${service.name}...`); 278 | const response = await axios.get(service.url(cleanIp)); 279 | 280 | // Check if the service returned an error 281 | if (response.data.error) { 282 | throw new Error(response.data.message || 'Service returned error'); 283 | } 284 | 285 | // Transform the data to our standard format 286 | return service.transform(response.data); 287 | } catch (error) { 288 | console.log(`${service.name} lookup failed:`, error.message); 289 | lastError = error; 290 | // Continue to next service 291 | continue; 292 | } 293 | } 294 | 295 | // If we get here, all services failed 296 | throw lastError; 297 | } 298 | 299 | // Universal lookup endpoint 300 | app.get('/api/lookup/:query', async (req, res) => { 301 | const query = req.params.query; 302 | const queryType = detectQueryType(query); 303 | 304 | try { 305 | let response; 306 | switch (queryType) { 307 | case 'whois': 308 | // Set specific options for WHOIS query 309 | const options = { 310 | follow: 3, // Follow up to 3 redirects 311 | timeout: 10000, // 10 second timeout 312 | }; 313 | 314 | // Add specific server for .eu domains 315 | if (query.toLowerCase().endsWith('.eu')) { 316 | options.server = 'whois.eu'; 317 | } 318 | 319 | const whoisData = await lookupPromise(query, options); 320 | const parsedData = await parseWhoisData(whoisData, query); 321 | 322 | response = { 323 | data: { 324 | ldhName: parsedData.domainName, 325 | handle: query, 326 | status: parsedData.status, 327 | ipAddresses: parsedData.ipAddresses, 328 | events: [ 329 | { 330 | eventAction: 'registration', 331 | eventDate: parsedData.creationDate 332 | }, 333 | { 334 | eventAction: 'expiration', 335 | eventDate: parsedData.expirationDate 336 | }, 337 | { 338 | eventAction: 'lastChanged', 339 | eventDate: parsedData.lastUpdated 340 | } 341 | ], 342 | nameservers: parsedData.nameservers.map(ns => ({ ldhName: ns })), 343 | entities: [{ 344 | roles: ['registrar'], 345 | vcardArray: [ 346 | "vcard", 347 | [ 348 | ["version", {}, "text", "4.0"], 349 | ["fn", {}, "text", parsedData.registrar], 350 | ["email", {}, "text", ""] 351 | ] 352 | ] 353 | }] 354 | } 355 | }; 356 | break; 357 | case 'ip': 358 | const ipData = await tryIpLookup(query); 359 | response = { data: ipData }; 360 | break; 361 | case 'asn': 362 | // Remove 'AS' prefix if present 363 | const asnNumber = query.replace(/^(AS|as)/i, ''); 364 | response = await axios.get(`https://api.bgpview.io/asn/${asnNumber}`); 365 | break; 366 | default: 367 | return res.status(400).json({ 368 | error: 'Invalid input', 369 | message: 'Please enter a valid domain name, IP address, or ASN number' 370 | }); 371 | } 372 | res.json({ type: queryType, data: response.data }); 373 | } catch (error) { 374 | console.error('Error details:', error); 375 | if (error.response) { 376 | if (error.response.status === 429) { 377 | res.status(429).json({ 378 | error: 'Rate limit exceeded', 379 | message: 'All IP lookup services are currently rate limited. Please try again later.' 380 | }); 381 | } else if (error.response.status === 404) { 382 | res.status(404).json({ error: `${queryType.toUpperCase()} not found` }); 383 | } else { 384 | res.status(error.response.status).json({ 385 | error: `Error fetching ${queryType.toUpperCase()} data`, 386 | message: error.response.data?.message || error.message 387 | }); 388 | } 389 | } else { 390 | res.status(500).json({ 391 | error: `Error fetching ${queryType.toUpperCase()} data`, 392 | message: error.message 393 | }); 394 | } 395 | } 396 | }); 397 | 398 | // Serve the pwa/asset manifest 399 | app.get('/asset-manifest.json', (req, res) => { 400 | // generated in pwa-manifest-generator and fetched from service-worker.js 401 | res.sendFile(path.join(ASSETS_DIR, 'asset-manifest.json')); 402 | }); 403 | app.get('/manifest.json', (req, res) => { 404 | res.sendFile(path.join(ASSETS_DIR, 'manifest.json')); 405 | }); 406 | 407 | app.get('/config', (req, res) => { 408 | res.json({ 409 | siteTitle: SITE_TITLE 410 | }); 411 | }); 412 | 413 | app.get('/managers/toast', (req, res) => { 414 | res.sendFile(path.join(PUBLIC_DIR, 'managers', 'toast.js')); 415 | }); 416 | 417 | app.get('*', (req, res) => { 418 | res.sendFile(path.join(__dirname, 'public', 'index.html')); 419 | }); 420 | 421 | app.listen(PORT, () => { 422 | console.log(`Server is running on: http://localhost:${PORT}`); 423 | }); --------------------------------------------------------------------------------