Files
whois-api/api/whois/[domain].ts
kappa 8d0c4d9da1 feat: 사설 ccSLD 미지원 처리 및 README 추가
- it.com, uk.com 등 CentralNic 사설 ccSLD 목록 추가
- 미지원 ccSLD 조회 시 적절한 안내 메시지 반환
- README.md 문서 작성 (API 사용법, 지원 TLD, ccSLD 안내)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 00:39:58 +09:00

258 lines
7.0 KiB
TypeScript

import { createConnection } from 'net';
import type { VercelRequest, VercelResponse } from '@vercel/node';
// WHOIS 미지원 사설 ccSLD 목록
const UNSUPPORTED_CCSLDS = [
'it.com', // it.com Domains Ltd (런던) - 사설 레지스트리
'uk.com', // CentralNic - WHOIS 미제공
'us.com', // CentralNic - WHOIS 미제공
'eu.com', // CentralNic - WHOIS 미제공
'de.com', // CentralNic - WHOIS 미제공
'br.com', // CentralNic - WHOIS 미제공
'cn.com', // CentralNic - WHOIS 미제공
'jpn.com', // CentralNic - WHOIS 미제공
'kr.com', // CentralNic - WHOIS 미제공
'ru.com', // CentralNic - WHOIS 미제공
'za.com', // CentralNic - WHOIS 미제공
];
function isUnsupportedCcSLD(domain: string): string | null {
const parts = domain.toLowerCase().split('.');
if (parts.length >= 3) {
const ccSLD = parts.slice(-2).join('.');
if (UNSUPPORTED_CCSLDS.includes(ccSLD)) {
return ccSLD;
}
}
return null;
}
// 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();
// 사설 ccSLD WHOIS 미지원 체크
const unsupportedCcSLD = isUnsupportedCcSLD(domainName);
if (unsupportedCcSLD) {
return res.status(200).json({
domain: domainName,
whois_supported: false,
ccSLD: unsupportedCcSLD,
message: `${unsupportedCcSLD} is a private ccSLD that does not provide public WHOIS data`,
message_ko: `${unsupportedCcSLD}은(는) 공개 WHOIS를 제공하지 않는 사설 ccSLD입니다`,
suggestion: 'Use registrar account API or contact the registry directly',
suggestion_ko: '등록기관 계정 API를 사용하거나 레지스트리에 직접 문의하세요',
query_time_ms: Date.now() - startTime,
});
}
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,
});
}
}