Add new tool: DNS 查询工具 — 在线查 A/AAAA/CNAME/MX/TXT 记录

This commit is contained in:
lidong-sal 2026-06-12 16:39:08 +08:00
parent f7eef656d7
commit cb327a5d87
2 changed files with 98 additions and 86 deletions

View file

@ -1,17 +1,25 @@
export const dnsRecordTypes = [
{ label: 'A', value: 'A' },
{ label: 'AAAA', value: 'AAAA' },
{ label: 'CNAME', value: 'CNAME' },
{ label: 'MX', value: 'MX' },
{ label: 'TXT', value: 'TXT' },
{ label: 'NS', value: 'NS' },
{ label: 'SOA', value: 'SOA' },
{ label: 'SRV', value: 'SRV' },
{ label: 'CAA', value: 'CAA' },
{ label: 'PTR', value: 'PTR' },
] as const;
export const defaultRecordTypes = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SOA'] as const;
export type DnsRecordType = typeof dnsRecordTypes[number]['value'];
export const allRecordTypes = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SOA', 'SRV', 'CAA', 'PTR'] as const;
export type DnsRecordType = typeof allRecordTypes[number];
const dnsTypeNumberToName: Record<number, string> = {
1: 'A',
2: 'NS',
5: 'CNAME',
6: 'SOA',
12: 'PTR',
15: 'MX',
16: 'TXT',
28: 'AAAA',
33: 'SRV',
257: 'CAA',
};
export function getTypeName(typeNumber: number): string {
return dnsTypeNumberToName[typeNumber] ?? `TYPE${typeNumber}`;
}
export interface DnsAnswer {
name: string
@ -32,20 +40,7 @@ export interface DnsResponse {
Authority?: DnsAnswer[]
}
const dnsStatusCodes: Record<number, string> = {
0: 'NOERROR',
1: 'FORMERR',
2: 'SERVFAIL',
3: 'NXDOMAIN',
4: 'NOTIMP',
5: 'REFUSED',
};
export function getDnsStatusText(status: number): string {
return dnsStatusCodes[status] ?? `UNKNOWN (${status})`;
}
export async function queryDns({ domain, type }: { domain: string; type: DnsRecordType }): Promise<DnsResponse> {
export async function queryDns({ domain, type }: { domain: string; type: string }): Promise<DnsResponse> {
const url = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(domain)}&type=${encodeURIComponent(type)}`;
const response = await fetch(url, {
@ -59,27 +54,37 @@ export async function queryDns({ domain, type }: { domain: string; type: DnsReco
return response.json();
}
export async function queryAllDns(domain: string, types: readonly string[]): Promise<DnsAnswer[]> {
const results = await Promise.allSettled(
types.map(type => queryDns({ domain, type })),
);
const seen = new Set<string>();
const answers: DnsAnswer[] = [];
for (const result of results) {
if (result.status === 'fulfilled' && result.value.Answer) {
for (const answer of result.value.Answer) {
const key = `${answer.type}|${answer.name}|${answer.data}`;
if (!seen.has(key)) {
seen.add(key);
answers.push(answer);
}
}
}
}
return answers;
}
export function formatDnsRecords(answers: DnsAnswer[]): string {
if (answers.length === 0) {
return 'No records found';
}
const lines = answers.map((answer) => {
const ttl = formatTTL(answer.TTL);
return `${answer.name} ${ttl} ${answer.data}`;
const typeName = getTypeName(answer.type);
return `${typeName}\t${answer.name}\t${answer.TTL}s\t${answer.data}`;
});
return lines.join('\n');
}
function formatTTL(seconds: number): string {
if (seconds < 60) {
return `${seconds}s`;
}
if (seconds < 3600) {
return `${Math.floor(seconds / 60)}m${seconds % 60 ? ` ${seconds % 60}s` : ''}`;
}
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
return `${h}h${m ? ` ${m}m` : ''}`;
}

View file

@ -1,14 +1,12 @@
<script setup lang="ts">
import { useCopy } from '@/composable/copy';
import { queryDns, formatDnsRecords, getDnsStatusText, dnsRecordTypes } from './dns-query.service';
import type { DnsAnswer, DnsRecordType } from './dns-query.service';
import { queryAllDns, formatDnsRecords, getTypeName, defaultRecordTypes } from './dns-query.service';
import type { DnsAnswer } from './dns-query.service';
const domain = ref('example.com');
const recordType = ref<DnsRecordType>('A');
const isLoading = ref(false);
const errorMessage = ref('');
const answers = ref<DnsAnswer[]>([]);
const statusText = ref('');
const hasQueried = ref(false);
const domainValidationRules = [
@ -18,6 +16,23 @@ const domainValidationRules = [
},
];
const groupedAnswers = computed(() => {
const groups: { type: string; records: DnsAnswer[] }[] = [];
const seen = new Map<string, DnsAnswer[]>();
for (const answer of answers.value) {
const typeName = getTypeName(answer.type);
if (!seen.has(typeName)) {
const records: DnsAnswer[] = [];
seen.set(typeName, records);
groups.push({ type: typeName, records });
}
seen.get(typeName)!.push(answer);
}
return groups;
});
const formattedResult = computed(() => formatDnsRecords(answers.value));
const { copy } = useCopy({ source: formattedResult, text: 'DNS records copied to the clipboard' });
@ -31,13 +46,10 @@ async function doQuery() {
isLoading.value = true;
errorMessage.value = '';
answers.value = [];
statusText.value = '';
hasQueried.value = true;
try {
const result = await queryDns({ domain: trimmed, type: recordType.value });
statusText.value = getDnsStatusText(result.Status);
answers.value = result.Answer ?? [];
answers.value = await queryAllDns(trimmed, defaultRecordTypes);
}
catch (err: unknown) {
errorMessage.value = err instanceof Error ? err.message : 'DNS query failed';
@ -63,15 +75,8 @@ async function doQuery() {
mb-4
/>
<c-select
v-model:value="recordType"
label="Record type"
:options="dnsRecordTypes"
mb-4
/>
<div flex justify-center mb-4>
<c-button :disabled="!domain.trim()" @click="doQuery()">
<c-button :disabled="!domain.trim() || isLoading" @click="doQuery()">
{{ isLoading ? 'Querying...' : 'Query DNS' }}
</c-button>
</div>
@ -81,41 +86,43 @@ async function doQuery() {
</n-alert>
<div v-if="hasQueried && !isLoading && !errorMessage">
<div mb-2 flex items-center gap-2>
<span font-bold>Status:</span>
<span>{{ statusText }}</span>
</div>
<div v-if="groupedAnswers.length > 0">
<div v-for="group in groupedAnswers" :key="group.type" mb-4>
<div mb-2 font-bold text-15px>
{{ group.type }}
</div>
<n-table :bordered="true" :single-line="false" size="small">
<thead>
<tr>
<th>Name</th>
<th>TTL</th>
<th>Data</th>
</tr>
</thead>
<tbody>
<tr v-for="(answer, index) in group.records" :key="index">
<td>{{ answer.name }}</td>
<td>{{ answer.TTL }}s</td>
<td style="word-break: break-all;">
{{ answer.data }}
</td>
</tr>
</tbody>
</n-table>
</div>
<n-table v-if="answers.length > 0" :bordered="true" :single-line="false" size="small" mb-4>
<thead>
<tr>
<th>Name</th>
<th>TTL</th>
<th>Data</th>
</tr>
</thead>
<tbody>
<tr v-for="(answer, index) in answers" :key="index">
<td>{{ answer.name }}</td>
<td>{{ answer.TTL }}s</td>
<td style="word-break: break-all;">
{{ answer.data }}
</td>
</tr>
</tbody>
</n-table>
<div flex justify-center>
<c-button @click="copy()">
Copy results
</c-button>
</div>
</div>
<c-card v-else mb-4>
<div italic op-60>
No records found for this domain and record type.
No records found for this domain.
</div>
</c-card>
<div v-if="answers.length > 0" flex justify-center>
<c-button @click="copy()">
Copy results
</c-button>
</div>
</div>
</div>
</template>