346 lines
11 KiB
TypeScript
346 lines
11 KiB
TypeScript
import * as crypto from "crypto";
|
|
import * as dns from "dns";
|
|
import { promisify } from "util";
|
|
|
|
import { EntityManager } from "@mikro-orm/core";
|
|
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
|
import { firstValueFrom } from "rxjs";
|
|
|
|
import { DnsRecordMessage, DomainMessage } from "../../../common/enums/message.enum";
|
|
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
|
import { CreateDnsRecordDto } from "../DTO/create-dns-record.dto";
|
|
import { DnsRecord } from "../entities/dns-record.entity";
|
|
import { Domain } from "../entities/domain.entity";
|
|
import { DNSRecordType, VerificationStatus } from "../enums/domain-status.enum";
|
|
import { DnsRecordRepository } from "../repositories/dns-record.repository";
|
|
import { DomainRepository } from "../repositories/domain.repository";
|
|
|
|
@Injectable()
|
|
export class DnsService {
|
|
private readonly logger = new Logger(DnsService.name);
|
|
|
|
private readonly mailServerDomain: string = "mail.danakcorp.com";
|
|
private readonly serverDomain: string = "danakcorp.com";
|
|
private readonly spfRecord: string = `v=spf1 a mx include:${this.serverDomain} ~all`;
|
|
// private readonly dmarcRecord: string = `v=DMARC1; p=quarantine; rua=mailto:dmarc@${this.serverDomain}; ruf=mailto:dmarc@${this.serverDomain}`;
|
|
|
|
private readonly resolveTxt = promisify(dns.resolveTxt);
|
|
private readonly resolveCname = promisify(dns.resolveCname);
|
|
private readonly resolveMx = promisify(dns.resolveMx);
|
|
|
|
constructor(
|
|
private readonly dnsRecordRepository: DnsRecordRepository,
|
|
private readonly domainRepository: DomainRepository,
|
|
private readonly mailServerService: MailServerService,
|
|
private readonly em: EntityManager,
|
|
) {}
|
|
|
|
/*******************************/
|
|
async createDnsRecord(createDnsRecordDto: CreateDnsRecordDto) {
|
|
const domain = await this.domainRepository.findOne({ id: createDnsRecordDto.domainId });
|
|
if (!domain) throw new BadRequestException(DomainMessage.DOMAIN_NOT_FOUND);
|
|
|
|
const dnsRecord = this.dnsRecordRepository.create({
|
|
...createDnsRecordDto,
|
|
domain,
|
|
});
|
|
|
|
await this.em.persistAndFlush(dnsRecord);
|
|
this.logger.log(`DNS record created: ${dnsRecord.name} ${dnsRecord.type}`);
|
|
|
|
return { dnsRecord };
|
|
}
|
|
|
|
/**
|
|
* Get DNS records for domain
|
|
*/
|
|
async getDomainDnsRecords(domainId: string) {
|
|
const dnsRecords = await this.dnsRecordRepository.findByDomainId(domainId);
|
|
return { dnsRecords };
|
|
}
|
|
|
|
/**
|
|
* Update DNS record
|
|
*/
|
|
async updateDnsRecord(id: string, updates: Partial<DnsRecord>) {
|
|
const dnsRecord = await this.dnsRecordRepository.findOne({ id });
|
|
if (!dnsRecord) throw new BadRequestException(DnsRecordMessage.DNS_RECORD_NOT_FOUND);
|
|
|
|
this.em.assign(dnsRecord, updates);
|
|
await this.em.persistAndFlush(dnsRecord);
|
|
|
|
return { dnsRecord };
|
|
}
|
|
|
|
/*******************************/
|
|
async deleteDnsRecord(id: string) {
|
|
const dnsRecord = await this.dnsRecordRepository.findOne({ id });
|
|
if (!dnsRecord) throw new BadRequestException(DnsRecordMessage.DNS_RECORD_NOT_FOUND);
|
|
|
|
dnsRecord.isActive = false;
|
|
dnsRecord.deletedAt = new Date();
|
|
await this.em.persistAndFlush(dnsRecord);
|
|
|
|
return { message: DnsRecordMessage.DNS_RECORD_DELETED };
|
|
}
|
|
|
|
/*******************************/
|
|
async generateRequiredDnsRecords(domain: Domain) {
|
|
const requiredRecords: Partial<DnsRecord>[] = [];
|
|
|
|
// MX Record
|
|
requiredRecords.push({
|
|
name: "@",
|
|
fullName: domain.name,
|
|
type: DNSRecordType.MX,
|
|
value: this.mailServerDomain,
|
|
priority: 10,
|
|
isRequired: true,
|
|
description: "Mail exchange record for email delivery",
|
|
});
|
|
|
|
// SPF Record
|
|
requiredRecords.push({
|
|
name: "@",
|
|
fullName: domain.name,
|
|
type: DNSRecordType.TXT,
|
|
value: this.spfRecord,
|
|
isRequired: true,
|
|
description: "Sender Policy Framework record",
|
|
});
|
|
|
|
// DMARC Record
|
|
const dmarcRecord = `v=DMARC1; p=quarantine; rua=mailto:dmarc@${domain.name}; ruf=mailto:dmarc@${domain.name}`;
|
|
requiredRecords.push({
|
|
name: `_dmarc`,
|
|
fullName: `_dmarc.${domain.name}`,
|
|
type: DNSRecordType.TXT,
|
|
value: dmarcRecord,
|
|
isRequired: true,
|
|
description: "DMARC policy record",
|
|
});
|
|
|
|
const dnsRecords: DnsRecord[] = [];
|
|
|
|
for (const recordData of requiredRecords) {
|
|
const dnsRecord = this.dnsRecordRepository.create({
|
|
name: recordData.name!,
|
|
fullName: recordData.fullName!,
|
|
type: recordData.type!,
|
|
value: recordData.value!,
|
|
domain,
|
|
isRequired: recordData.isRequired,
|
|
description: recordData.description,
|
|
priority: recordData.priority,
|
|
ttl: recordData.ttl,
|
|
});
|
|
dnsRecords.push(dnsRecord);
|
|
}
|
|
|
|
await this.em.persistAndFlush(dnsRecords);
|
|
this.logger.log(`Generated ${dnsRecords.length} required DNS records for ${domain.name}`);
|
|
|
|
return { dnsRecords };
|
|
}
|
|
|
|
/**
|
|
* Create DKIM record
|
|
*/
|
|
async createDKIMRecord(domain: Domain, selector: string, dkimName: string, dkimFullName: string, dkimValue: string, wildduckId: string) {
|
|
const dkimRecord = this.dnsRecordRepository.create({
|
|
name: dkimName,
|
|
fullName: dkimFullName,
|
|
type: DNSRecordType.TXT,
|
|
value: dkimValue,
|
|
domain,
|
|
isRequired: true,
|
|
description: `DKIM public key for selector ${selector}`,
|
|
wildduckId,
|
|
});
|
|
|
|
await this.em.persistAndFlush(dkimRecord);
|
|
this.logger.log(`DKIM record created for ${domain.name} with selector ${selector}`);
|
|
|
|
return { dkimRecord };
|
|
}
|
|
|
|
/**
|
|
* Generate DKIM keys
|
|
*/
|
|
async generateDKIMKeysByMailServer(domainName: string, selector: string) {
|
|
const data = await firstValueFrom(this.mailServerService.dkim.createDKIMKey({ domain: domainName, selector }));
|
|
return { publicKey: data.publicKey, privateKey: data.dnsTxt.value, dnsTxt: data.dnsTxt, wildduckId: data.id };
|
|
}
|
|
|
|
/**
|
|
* Generate DKIM keys native
|
|
*/
|
|
async generateDKIMKeysByNative() {
|
|
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
|
|
modulusLength: 2048,
|
|
publicKeyEncoding: {
|
|
type: "spki",
|
|
format: "pem",
|
|
},
|
|
privateKeyEncoding: {
|
|
type: "pkcs8",
|
|
format: "pem",
|
|
},
|
|
});
|
|
|
|
// Extract the public key without headers and newlines for DNS
|
|
const publicKeyForDNS = publicKey
|
|
.replace(/-----BEGIN PUBLIC KEY-----/g, "")
|
|
.replace(/-----END PUBLIC KEY-----/g, "")
|
|
.replace(/\n/g, "");
|
|
|
|
return {
|
|
privateKey,
|
|
publicKey: publicKeyForDNS,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Verify domain DNS configuration
|
|
*/
|
|
async verifyDomainDNS(domain: Domain) {
|
|
const dnsRecords = await this.dnsRecordRepository.findByDomainId(domain.id);
|
|
const requiredRecords = dnsRecords.filter((record) => record.isRequired && record.isActive);
|
|
|
|
let allVerified = true;
|
|
|
|
for (const record of requiredRecords) {
|
|
const isVerified = await this.verifyDnsRecord(record);
|
|
if (!isVerified) {
|
|
allVerified = false;
|
|
}
|
|
}
|
|
|
|
return allVerified;
|
|
}
|
|
|
|
/**
|
|
* Verify individual DNS record
|
|
*/
|
|
async verifyDnsRecord(dnsRecord: DnsRecord) {
|
|
try {
|
|
dnsRecord.verificationAttempts += 1;
|
|
dnsRecord.lastCheckedAt = new Date();
|
|
|
|
let isVerified = false;
|
|
|
|
switch (dnsRecord.type) {
|
|
case DNSRecordType.TXT:
|
|
case DNSRecordType.SPF:
|
|
case DNSRecordType.DMARC:
|
|
case DNSRecordType.DKIM:
|
|
isVerified = await this.verifyTxtRecord(dnsRecord);
|
|
break;
|
|
case DNSRecordType.MX:
|
|
isVerified = await this.verifyMxRecord(dnsRecord);
|
|
break;
|
|
case DNSRecordType.CNAME:
|
|
isVerified = await this.verifyCnameRecord(dnsRecord);
|
|
break;
|
|
default:
|
|
this.logger.warn(`DNS record type ${dnsRecord.type} verification not implemented`);
|
|
isVerified = false;
|
|
}
|
|
|
|
if (isVerified) {
|
|
dnsRecord.status = VerificationStatus.VERIFIED;
|
|
dnsRecord.lastVerifiedAt = new Date();
|
|
dnsRecord.errorMessage = undefined;
|
|
} else {
|
|
dnsRecord.status = VerificationStatus.FAILED;
|
|
dnsRecord.errorMessage = "DNS record not found or value mismatch";
|
|
}
|
|
|
|
await this.em.persistAndFlush(dnsRecord);
|
|
return isVerified;
|
|
} catch (error) {
|
|
dnsRecord.status = VerificationStatus.FAILED;
|
|
dnsRecord.errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
await this.em.persistAndFlush(dnsRecord);
|
|
|
|
this.logger.error(`DNS verification failed for ${dnsRecord.fullName}:`, error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify TXT record
|
|
*/
|
|
private async verifyTxtRecord(dnsRecord: DnsRecord) {
|
|
try {
|
|
const txtRecords = await this.resolveTxt(dnsRecord.fullName);
|
|
const flatRecords = txtRecords.flat();
|
|
|
|
// Join all TXT record parts together (handles multi-part TXT records like DKIM)
|
|
const joinedRecord = flatRecords.join("");
|
|
|
|
// Check if the expected value matches either the joined record or any individual part
|
|
return joinedRecord === dnsRecord.value || flatRecords.some((record) => record.includes(dnsRecord.value) || record === dnsRecord.value);
|
|
} catch (error) {
|
|
this.logger.debug(`TXT record verification failed for ${dnsRecord.fullName}:`, error instanceof Error ? error.message : "Unknown error");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify MX record
|
|
*/
|
|
private async verifyMxRecord(dnsRecord: DnsRecord) {
|
|
try {
|
|
const mxRecords = await this.resolveMx(dnsRecord.fullName);
|
|
|
|
return mxRecords.some((mx) => mx.exchange === dnsRecord.value && mx.priority === (dnsRecord.priority || 10));
|
|
} catch (error) {
|
|
this.logger.debug(`MX record verification failed for ${dnsRecord.name}:`, error instanceof Error ? error.message : "Unknown error");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify CNAME record
|
|
*/
|
|
private async verifyCnameRecord(dnsRecord: DnsRecord) {
|
|
try {
|
|
const cnameRecords = await this.resolveCname(dnsRecord.fullName);
|
|
|
|
return cnameRecords.includes(dnsRecord.value);
|
|
} catch (error) {
|
|
this.logger.debug(`CNAME record verification failed for ${dnsRecord.name}:`, error instanceof Error ? error.message : "Unknown error");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get DNS record statistics
|
|
*/
|
|
async getDnsRecordStats(domainId: string): Promise<{
|
|
total: number;
|
|
verified: number;
|
|
pending: number;
|
|
failed: number;
|
|
required: number;
|
|
}> {
|
|
return this.dnsRecordRepository.getDomainDnsStats(domainId);
|
|
}
|
|
|
|
/**
|
|
* Verify all DNS records that need checking
|
|
*/
|
|
async verifyPendingDnsRecords(): Promise<void> {
|
|
const records = await this.dnsRecordRepository.findNeedingVerification();
|
|
|
|
for (const record of records) {
|
|
try {
|
|
await this.verifyDnsRecord(record);
|
|
} catch (error) {
|
|
this.logger.error(`Failed to verify DNS record ${record.name}:`, error);
|
|
}
|
|
}
|
|
}
|
|
}
|