mirror of
https://github.com/CorentinTh/it-tools.git
synced 2026-08-04 14:52:41 +00:00
Add new tool: DNS 查询工具 — 在线查 A/AAAA/CNAME/MX/TXT 记录
This commit is contained in:
parent
f7eef656d7
commit
cb327a5d87
2 changed files with 98 additions and 86 deletions
|
|
@ -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` : ''}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue