feat: WHOIS API 서버 초기 구현
- Vercel Serverless Functions 기반 - TCP 포트 43 직접 연결로 모든 TLD 지원 - TLD별 WHOIS 서버 매핑 (~50개) - 리퍼럴 서버 자동 추적 - JSON 응답 (raw WHOIS 포함) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
.vercel/
|
||||
dist/
|
||||
*.log
|
||||
.env
|
||||
.env.local
|
||||
.vercel
|
||||
216
api/whois/[domain].ts
Normal file
216
api/whois/[domain].ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { createConnection } from 'net';
|
||||
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
||||
|
||||
// TLD to WHOIS server mapping
|
||||
const WHOIS_SERVERS: Record<string, string> = {
|
||||
// Generic TLDs
|
||||
com: 'whois.verisign-grs.com',
|
||||
net: 'whois.verisign-grs.com',
|
||||
org: 'whois.pir.org',
|
||||
info: 'whois.afilias.net',
|
||||
biz: 'whois.biz',
|
||||
|
||||
// Popular new gTLDs
|
||||
io: 'whois.nic.io',
|
||||
co: 'whois.registry.co',
|
||||
me: 'whois.nic.me',
|
||||
tv: 'whois.nic.tv',
|
||||
cc: 'whois.nic.cc',
|
||||
app: 'whois.nic.google',
|
||||
dev: 'whois.nic.google',
|
||||
xyz: 'whois.nic.xyz',
|
||||
online: 'whois.nic.online',
|
||||
site: 'whois.nic.site',
|
||||
tech: 'whois.nic.tech',
|
||||
shop: 'whois.nic.shop',
|
||||
blog: 'whois.nic.blog',
|
||||
club: 'whois.nic.club',
|
||||
live: 'whois.nic.live',
|
||||
cloud: 'whois.nic.cloud',
|
||||
|
||||
// Country Code TLDs
|
||||
kr: 'whois.kr',
|
||||
jp: 'whois.jprs.jp',
|
||||
cn: 'whois.cnnic.cn',
|
||||
uk: 'whois.nic.uk',
|
||||
de: 'whois.denic.de',
|
||||
fr: 'whois.nic.fr',
|
||||
it: 'whois.nic.it',
|
||||
es: 'whois.nic.es',
|
||||
nl: 'whois.domain-registry.nl',
|
||||
ru: 'whois.tcinet.ru',
|
||||
ca: 'whois.cira.ca',
|
||||
in: 'whois.registry.in',
|
||||
mx: 'whois.mx',
|
||||
cv: 'whois.nic.cv',
|
||||
au: 'whois.auda.org.au',
|
||||
br: 'whois.registro.br',
|
||||
|
||||
// ccSLDs
|
||||
'it.com': 'whois.centralnic.com',
|
||||
'uk.com': 'whois.centralnic.com',
|
||||
'us.com': 'whois.centralnic.com',
|
||||
'eu.com': 'whois.centralnic.com',
|
||||
'co.kr': 'whois.kr',
|
||||
'or.kr': 'whois.kr',
|
||||
'co.uk': 'whois.nic.uk',
|
||||
'org.uk': 'whois.nic.uk',
|
||||
'com.au': 'whois.auda.org.au',
|
||||
};
|
||||
|
||||
function getWhoisServer(domain: string): string {
|
||||
const parts = domain.toLowerCase().split('.');
|
||||
|
||||
// Check ccSLD first (e.g., co.kr, it.com)
|
||||
if (parts.length >= 3) {
|
||||
const ccSLD = parts.slice(-2).join('.');
|
||||
if (WHOIS_SERVERS[ccSLD]) {
|
||||
return WHOIS_SERVERS[ccSLD];
|
||||
}
|
||||
}
|
||||
|
||||
// Then check TLD
|
||||
const tld = parts[parts.length - 1];
|
||||
return WHOIS_SERVERS[tld] || 'whois.iana.org';
|
||||
}
|
||||
|
||||
function queryWhois(server: string, domain: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
// Special query formats for some servers
|
||||
let query = domain;
|
||||
if (server.includes('verisign')) {
|
||||
query = '=' + domain;
|
||||
} else if (server.includes('denic.de')) {
|
||||
query = '-T dn,ace ' + domain;
|
||||
} else if (server.includes('jprs.jp')) {
|
||||
query = domain + '/e';
|
||||
}
|
||||
|
||||
const socket = createConnection(43, server, () => {
|
||||
socket.write(query + '\r\n');
|
||||
});
|
||||
|
||||
socket.setTimeout(15000);
|
||||
|
||||
socket.on('data', (data) => {
|
||||
chunks.push(data);
|
||||
});
|
||||
|
||||
socket.on('end', () => {
|
||||
resolve(Buffer.concat(chunks).toString('utf-8'));
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
socket.destroy();
|
||||
reject(new Error('Connection timeout'));
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function extractReferral(raw: string): string | null {
|
||||
const patterns = [
|
||||
/Registrar WHOIS Server:\s*(.+)/i,
|
||||
/Whois Server:\s*(.+)/i,
|
||||
/ReferralServer:\s*whois:\/\/(.+)/i,
|
||||
/refer:\s*(.+)/i,
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = raw.match(pattern);
|
||||
if (match && match[1]) {
|
||||
const server = match[1].trim().replace('whois://', '');
|
||||
if (server && !server.includes(' ')) {
|
||||
return server;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkAvailability(raw: string): boolean {
|
||||
const availablePatterns = [
|
||||
'no match for',
|
||||
'not found',
|
||||
'no data found',
|
||||
'domain not found',
|
||||
'no entries found',
|
||||
'status: available',
|
||||
'the queried object does not exist',
|
||||
'is available for registration',
|
||||
'this domain name has not been registered',
|
||||
];
|
||||
|
||||
const rawLower = raw.toLowerCase();
|
||||
return availablePatterns.some(p => rawLower.includes(p));
|
||||
}
|
||||
|
||||
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||
// CORS
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'X-API-Key, Content-Type');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
return res.status(200).end();
|
||||
}
|
||||
|
||||
// API Key check (optional)
|
||||
const apiKey = process.env.API_KEY;
|
||||
if (apiKey && req.headers['x-api-key'] !== apiKey) {
|
||||
return res.status(401).json({ error: 'Invalid API key' });
|
||||
}
|
||||
|
||||
const { domain } = req.query;
|
||||
|
||||
if (!domain || typeof domain !== 'string') {
|
||||
return res.status(400).json({ error: 'Domain parameter required' });
|
||||
}
|
||||
|
||||
const domainName = domain.toLowerCase().trim();
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const whoisServer = getWhoisServer(domainName);
|
||||
let raw = await queryWhois(whoisServer, domainName);
|
||||
let usedServer = whoisServer;
|
||||
|
||||
// Follow referral if exists
|
||||
const referral = extractReferral(raw);
|
||||
if (referral && referral !== whoisServer) {
|
||||
try {
|
||||
const referralRaw = await queryWhois(referral, domainName);
|
||||
if (referralRaw.length > raw.length / 2) {
|
||||
raw = referralRaw;
|
||||
usedServer = referral;
|
||||
}
|
||||
} catch {
|
||||
// Keep original response if referral fails
|
||||
}
|
||||
}
|
||||
|
||||
const available = checkAvailability(raw);
|
||||
const queryTime = Date.now() - startTime;
|
||||
|
||||
return res.status(200).json({
|
||||
domain: domainName,
|
||||
available,
|
||||
whois_server: usedServer,
|
||||
raw,
|
||||
query_time_ms: queryTime,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
const queryTime = Date.now() - startTime;
|
||||
return res.status(500).json({
|
||||
domain: domainName,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
query_time_ms: queryTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
3255
package-lock.json
generated
Normal file
3255
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
package.json
Normal file
15
package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "whois-api",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vercel dev",
|
||||
"deploy": "vercel --prod"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vercel": "^37.0.0"
|
||||
}
|
||||
}
|
||||
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["api/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
8
vercel.json
Normal file
8
vercel.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"version": 2,
|
||||
"functions": {
|
||||
"api/**/*.ts": {
|
||||
"maxDuration": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user