Files
AriWeb-Suite/modules/arimail/server/dns/common.mjs
T

53 lines
3.6 KiB
JavaScript
Raw Normal View History

/**
* DNS 모듈 공통 도구(1.0.5): 이름 정규화, TXT 분할, 오류 객체, 공개 DNS 조회(반영 확인).
*/
import { Resolver } from 'node:dns/promises';
/** FQDN(끝에 점). 빈 값은 그대로. */
export const fqdn = (name) => { const s = String(name || '').trim().toLowerCase(); return !s ? s : s.endsWith('.') ? s : s + '.'; };
/** 끝 점 없는 소문자 이름(비교용). */
export const bare = (name) => fqdn(name).replace(/\.$/, '');
/** TXT 값을 255자 이하 조각으로 나눕니다(RFC 1035 문자열 한도). */
export const txtChunks = (value) => { const s = String(value ?? ''); const out = []; for (let i = 0; i < Math.max(1, s.length); i += 255) out.push(s.slice(i, i + 255)); return out; };
/** dns_* 코드를 가진 Error. detail 은 화면에 그대로 보입니다. */
export function dnsError(code, detail) { const e = new Error(code); e.detail = String(detail || '').slice(0, 300); return e; }
/**
* 공개 리졸버(Google 8.8.8.8, Cloudflare 1.1.1.1)로 레코드가 실제로 보이는지 확인합니다. 권위 서버가 아니라 "세상이 보는 값" 기준이라
* DNS 업체에 적용한 직후에는 아직 안 보일 수 있습니다(캐시·전파). 그 경우 state 'wait' 로 알립니다.
* @param {object} record { name, type, values[], priority? }
* @param {object} [o] resolver: 시험용 대체 객체({ resolveTxt, resolveMx, resolveCname, resolve4 })
* @returns {Promise<{ok:boolean, found:string[], error?:string}>}
*/
export async function lookup(record, { resolver = null, servers = ['8.8.8.8', '1.1.1.1'] } = {}) {
const r = resolver || (() => { const x = new Resolver({ timeout: 4000, tries: 2 }); x.setServers(servers); return x; })();
const name = bare(record.name);
try {
if (record.type === 'TXT') {
const found = (await r.resolveTxt(name)).map((chunks) => chunks.join(''));
const ok = record.values.every((v) => found.some((f) => f.replace(/\s+/g, ' ').trim() === String(v).replace(/\s+/g, ' ').trim()));
return { ok, found };
}
if (record.type === 'MX') {
const found = (await r.resolveMx(name)).map((m) => `${m.priority} ${bare(m.exchange)}`);
const want = record.values.map((v) => `${record.priority ?? 10} ${bare(v)}`);
const ok = want.every((w) => found.includes(w));
return { ok, found };
}
if (record.type === 'CNAME') { const found = (await r.resolveCname(name)).map(bare); return { ok: record.values.every((v) => found.includes(bare(v))), found }; }
if (record.type === 'A') { const found = await r.resolve4(name); return { ok: record.values.every((v) => found.includes(v)), found }; }
return { ok: false, found: [], error: 'unsupported_type' };
} catch (e) {
// ENOTFOUND/ENODATA = 아직 없음(전파 대기), 그 밖은 조회 실패
return { ok: false, found: [], error: e.code === 'ENOTFOUND' || e.code === 'ENODATA' ? 'not_found' : e.code || e.message };
}
}
/** 호스트 이름의 A 레코드와 그 IP 의 PTR(역방향)을 함께 봅니다(Haraka 발신 준비 점검용). */
export async function reverseCheck(hostname, { resolver = null, servers = ['8.8.8.8', '1.1.1.1'] } = {}) {
const r = resolver || (() => { const x = new Resolver({ timeout: 4000, tries: 2 }); x.setServers(servers); return x; })();
let ips = []; try { ips = await r.resolve4(bare(hostname)); } catch (e) { return { ok: false, ips: [], ptr: [], error: 'a_' + (e.code || 'failed') }; }
const ptr = []; for (const ip of ips) { try { ptr.push(...(await r.reverse(ip)).map(bare)); } catch { /* PTR 없음 */ } }
return { ok: ips.length > 0 && ptr.includes(bare(hostname)), ips, ptr };
}