2026-05-05 21:56:08 +08:00
|
|
|
import { http, logger } from "@certd/basic";
|
2026-04-06 00:33:08 +08:00
|
|
|
import { parseDomainByPsl } from "@certd/plugin-lib";
|
2026-05-05 21:56:08 +08:00
|
|
|
import dayjs from "dayjs";
|
|
|
|
|
import customParseFormat from "dayjs/plugin/customParseFormat.js";
|
|
|
|
|
|
2026-05-05 22:53:22 +08:00
|
|
|
import { RdapSsClient } from "./rdap-ss-client.js";
|
|
|
|
|
|
2026-05-05 21:56:08 +08:00
|
|
|
dayjs.extend(customParseFormat);
|
2026-04-06 00:33:08 +08:00
|
|
|
|
|
|
|
|
export interface DomainInfo {
|
|
|
|
|
expirationDate?: number;
|
|
|
|
|
registrationDate?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class TldClient {
|
2026-05-05 22:53:22 +08:00
|
|
|
private rdapSsClient = new RdapSsClient();
|
2026-05-05 21:56:08 +08:00
|
|
|
private rdapMap: Record<string, string> = {};
|
2026-04-06 00:33:08 +08:00
|
|
|
private isInitialized = false;
|
|
|
|
|
|
2026-05-05 21:56:08 +08:00
|
|
|
constructor() {}
|
2026-04-06 00:33:08 +08:00
|
|
|
|
|
|
|
|
async init() {
|
|
|
|
|
if (this.isInitialized) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const dnsJson = await http.request({
|
|
|
|
|
url: "https://data.iana.org/rdap/dns.json",
|
|
|
|
|
method: "GET",
|
2026-05-05 21:56:08 +08:00
|
|
|
});
|
2026-04-06 00:33:08 +08:00
|
|
|
for (const item of dnsJson.services) {
|
2026-05-05 21:56:08 +08:00
|
|
|
const suffixes = item[0];
|
|
|
|
|
const urls = item[1];
|
2026-04-06 00:33:08 +08:00
|
|
|
for (const suffix of suffixes) {
|
2026-05-05 21:56:08 +08:00
|
|
|
this.rdapMap[suffix] = urls[0];
|
2026-04-06 00:33:08 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.isInitialized = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getDomainExpirationDate(domain: string): Promise<DomainInfo> {
|
|
|
|
|
await this.init();
|
2026-05-05 21:56:08 +08:00
|
|
|
|
|
|
|
|
const parsed = parseDomainByPsl(domain);
|
|
|
|
|
const mainDomain = parsed.domain || "";
|
2026-04-06 00:33:08 +08:00
|
|
|
if (mainDomain !== domain) {
|
2026-05-05 21:56:08 +08:00
|
|
|
const message = `【${domain}】为子域名,无法获取过期时间`;
|
|
|
|
|
logger.warn(message);
|
|
|
|
|
throw new Error(message);
|
2026-04-06 00:33:08 +08:00
|
|
|
}
|
2026-05-05 21:56:08 +08:00
|
|
|
|
2026-04-06 00:33:08 +08:00
|
|
|
try {
|
2026-05-05 21:56:08 +08:00
|
|
|
return await this.getDomainExpirationByRdap(domain, parsed.tld || "");
|
2026-04-06 00:33:08 +08:00
|
|
|
} catch (error) {
|
2026-05-05 21:56:08 +08:00
|
|
|
logger.error(this.getErrorMessage(error));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return await this.getDomainExpirationByWhoiser(domain);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(this.getErrorMessage(error));
|
|
|
|
|
return await this.getDomainExpirationByRdapSs(domain);
|
2026-04-06 00:33:08 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getDomainExpirationByRdap(domain: string, suffix: string): Promise<DomainInfo> {
|
2026-05-05 21:56:08 +08:00
|
|
|
const rdapUrl = this.rdapMap[suffix];
|
2026-04-06 00:33:08 +08:00
|
|
|
if (!rdapUrl) {
|
2026-05-05 21:56:08 +08:00
|
|
|
throw new Error(`【${domain}】未找到${suffix}的rdap地址`);
|
2026-04-06 00:33:08 +08:00
|
|
|
}
|
2026-05-05 21:56:08 +08:00
|
|
|
|
2026-04-06 00:33:08 +08:00
|
|
|
const rdap = await http.request({
|
|
|
|
|
url: `${rdapUrl}domain/${domain}`,
|
|
|
|
|
method: "GET",
|
2026-05-05 21:56:08 +08:00
|
|
|
});
|
2026-04-06 00:33:08 +08:00
|
|
|
|
2026-05-05 21:56:08 +08:00
|
|
|
let res: DomainInfo = {};
|
|
|
|
|
const events = rdap.events || [];
|
2026-04-06 00:33:08 +08:00
|
|
|
for (const item of events) {
|
2026-05-05 21:56:08 +08:00
|
|
|
if (item.eventAction === "expiration") {
|
|
|
|
|
res.expirationDate = dayjs(item.eventDate).valueOf();
|
|
|
|
|
} else if (item.eventAction === "registration") {
|
|
|
|
|
res.registrationDate = dayjs(item.eventDate).valueOf();
|
2026-04-06 00:33:08 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-05 21:56:08 +08:00
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getDomainExpirationByRdapSs(domain: string): Promise<DomainInfo> {
|
2026-05-05 22:53:22 +08:00
|
|
|
return await this.rdapSsClient.getDomainInfo(domain);
|
2026-04-06 00:33:08 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getDomainExpirationByWhoiser(domain: string): Promise<DomainInfo> {
|
2026-05-05 21:56:08 +08:00
|
|
|
const whoiser = await import("whoiser");
|
2026-04-06 00:33:08 +08:00
|
|
|
const result = await whoiser.whoisDomain(domain, {
|
|
|
|
|
follow: 2,
|
2026-05-05 21:56:08 +08:00
|
|
|
timeout: 5000,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let res: DomainInfo = {};
|
2026-04-06 00:33:08 +08:00
|
|
|
/**
|
|
|
|
|
* {
|
|
|
|
|
"Domain Status": [
|
|
|
|
|
"ok",
|
|
|
|
|
],
|
|
|
|
|
"Name Server": [
|
|
|
|
|
"dns21.hichina.com",
|
|
|
|
|
"dns22.hichina.com",
|
|
|
|
|
],
|
|
|
|
|
text: [
|
|
|
|
|
"",
|
|
|
|
|
],
|
|
|
|
|
"Domain Name": "docmirror.cn",
|
|
|
|
|
ROID: "20200907s10001s31265717-cn",
|
|
|
|
|
"Registrant Name": "肖君诺",
|
|
|
|
|
"Registrant Email": "252959493@qq.com",
|
|
|
|
|
Registrar: "阿里巴巴云计算(北京)有限公司",
|
|
|
|
|
"Created Date": "2020-09-07 09:22:54",
|
|
|
|
|
"Expiry Date": "2026-09-07 09:22:54",
|
|
|
|
|
DNSSEC: "unsigned",
|
|
|
|
|
}
|
|
|
|
|
*/
|
2026-05-05 21:56:08 +08:00
|
|
|
|
2026-04-06 00:33:08 +08:00
|
|
|
for (const server in result) {
|
2026-05-05 21:56:08 +08:00
|
|
|
const data = result[server] as any;
|
|
|
|
|
if (data["Expiry Date"]) {
|
|
|
|
|
res.expirationDate = dayjs(data["Expiry Date"]).valueOf();
|
2026-04-06 00:33:08 +08:00
|
|
|
}
|
2026-05-05 21:56:08 +08:00
|
|
|
if (data["Created Date"]) {
|
|
|
|
|
res.registrationDate = dayjs(data["Created Date"]).valueOf();
|
2026-04-06 00:33:08 +08:00
|
|
|
}
|
|
|
|
|
if (res.expirationDate && res.registrationDate) {
|
2026-05-05 21:56:08 +08:00
|
|
|
break;
|
2026-04-06 00:33:08 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-05 21:56:08 +08:00
|
|
|
|
2026-04-06 00:33:08 +08:00
|
|
|
if (!res.expirationDate) {
|
2026-05-05 21:56:08 +08:00
|
|
|
throw new Error(`【${domain}】whois查询未找到过期时间`);
|
2026-04-06 00:33:08 +08:00
|
|
|
}
|
2026-05-05 21:56:08 +08:00
|
|
|
|
|
|
|
|
return res;
|
2026-04-06 00:33:08 +08:00
|
|
|
}
|
2026-05-05 21:56:08 +08:00
|
|
|
|
|
|
|
|
private getErrorMessage(error: unknown) {
|
|
|
|
|
if (error instanceof Error) {
|
|
|
|
|
return error.message;
|
|
|
|
|
}
|
|
|
|
|
return String(error);
|
|
|
|
|
}
|
|
|
|
|
}
|