- it.com, uk.com 등 CentralNic 사설 ccSLD 목록 추가 - 미지원 ccSLD 조회 시 적절한 안내 메시지 반환 - README.md 문서 작성 (API 사용법, 지원 TLD, ccSLD 안내) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
258 lines
7.0 KiB
TypeScript
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,
|
|
});
|
|
}
|
|
}
|