chore: first commit
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
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 spfRecord: string = `v=spf1 include:${this.mailServerDomain} ~all`;
|
||||
private readonly dmarcRecord: string = `v=DMARC1; p=quarantine; rua=mailto:dmarc@${this.mailServerDomain}; ruf=mailto:dmarc@${this.mailServerDomain}`;
|
||||
|
||||
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: domain.name,
|
||||
type: DNSRecordType.MX,
|
||||
value: this.mailServerDomain,
|
||||
priority: 10,
|
||||
isRequired: true,
|
||||
description: "Mail exchange record for email delivery",
|
||||
});
|
||||
|
||||
// SPF Record
|
||||
requiredRecords.push({
|
||||
name: domain.name,
|
||||
type: DNSRecordType.SPF,
|
||||
value: this.spfRecord,
|
||||
isRequired: true,
|
||||
description: "Sender Policy Framework record",
|
||||
});
|
||||
|
||||
// DMARC Record
|
||||
requiredRecords.push({
|
||||
name: `_dmarc.${domain.name}`,
|
||||
type: DNSRecordType.DMARC,
|
||||
value: this.dmarcRecord,
|
||||
isRequired: true,
|
||||
description: "DMARC policy record",
|
||||
});
|
||||
|
||||
const dnsRecords: DnsRecord[] = [];
|
||||
|
||||
for (const recordData of requiredRecords) {
|
||||
const dnsRecord = this.dnsRecordRepository.create({
|
||||
name: recordData.name!,
|
||||
type: recordData.type!,
|
||||
value: recordData.value!,
|
||||
domain,
|
||||
isRequired: recordData.isRequired ?? true,
|
||||
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, dkimValue: string, wildduckId: string) {
|
||||
const dkimRecord = this.dnsRecordRepository.create({
|
||||
name: dkimName,
|
||||
type: DNSRecordType.DKIM,
|
||||
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.name}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify TXT record
|
||||
*/
|
||||
private async verifyTxtRecord(dnsRecord: DnsRecord) {
|
||||
try {
|
||||
const txtRecords = await this.resolveTxt(dnsRecord.name);
|
||||
const flatRecords = txtRecords.flat();
|
||||
|
||||
return flatRecords.some((record) => record.includes(dnsRecord.value) || record === dnsRecord.value);
|
||||
} catch (error) {
|
||||
this.logger.debug(`TXT record verification failed for ${dnsRecord.name}:`, error instanceof Error ? error.message : "Unknown error");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify MX record
|
||||
*/
|
||||
private async verifyMxRecord(dnsRecord: DnsRecord) {
|
||||
try {
|
||||
const mxRecords = await this.resolveMx(dnsRecord.name);
|
||||
|
||||
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.name);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user