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