Files
onebox/ts/classes/dns.ts
2025-11-18 00:03:24 +00:00

302 lines
9.1 KiB
TypeScript

/**
* DNS Manager for Onebox
*
* Manages DNS records via Cloudflare API
*/
import * as plugins from '../plugins.ts';
import { logger } from '../logging.ts';
import { OneboxDatabase } from './database.ts';
export class OneboxDnsManager {
private oneboxRef: any;
private database: OneboxDatabase;
private cloudflareClient: plugins.cloudflare.CloudflareAccount | null = null;
private zoneID: string | null = null;
private serverIP: string | null = null;
constructor(oneboxRef: any) {
this.oneboxRef = oneboxRef;
this.database = oneboxRef.database;
}
/**
* Initialize DNS manager with Cloudflare credentials
*/
async init(): Promise<void> {
try {
// Get Cloudflare credentials from settings
const apiKey = this.database.getSetting('cloudflareAPIKey');
const email = this.database.getSetting('cloudflareEmail');
const serverIP = this.database.getSetting('serverIP');
if (!apiKey || !email) {
logger.warn('Cloudflare credentials not configured. DNS management will be disabled.');
logger.info('Configure with: onebox config set cloudflareAPIKey <key>');
logger.info('Configure with: onebox config set cloudflareEmail <email>');
return;
}
this.serverIP = serverIP;
// Initialize Cloudflare client
// The CloudflareAccount class expects just the API key/token
this.cloudflareClient = new plugins.cloudflare.CloudflareAccount(apiKey);
// Try to get zone ID from settings, or auto-detect
let zoneID = this.database.getSetting('cloudflareZoneID');
if (!zoneID) {
// Auto-detect zones
logger.info('No zone ID configured, fetching available zones...');
const zones = await this.cloudflareClient.convenience.listZones();
if (zones.length === 0) {
logger.warn('No Cloudflare zones found for this account');
return;
} else if (zones.length === 1) {
// Auto-select the only zone
zoneID = zones[0].id;
this.database.setSetting('cloudflareZoneID', zoneID);
logger.success(`Auto-selected zone: ${zones[0].name} (${zoneID})`);
} else {
// Multiple zones found - log them for user to choose
logger.info(`Found ${zones.length} Cloudflare zones:`);
for (const zone of zones) {
logger.info(` - ${zone.name} (ID: ${zone.id})`);
}
logger.warn('Multiple zones found. Please set one with: onebox config set cloudflareZoneID <id>');
return;
}
}
this.zoneID = zoneID;
logger.info('DNS manager initialized with Cloudflare');
} catch (error) {
logger.error(`Failed to initialize DNS manager: ${error.message}`);
if (error.message && error.message.includes('Authorization header')) {
logger.error('The provided API key appears to be invalid.');
logger.error('Make sure you are using a Cloudflare API TOKEN (not the global API key).');
logger.info('Create an API Token at: https://dash.cloudflare.com/profile/api-tokens');
logger.info('The token needs "Zone:Read" and "DNS:Edit" permissions.');
}
throw error;
}
}
/**
* Check if DNS manager is configured
*/
isConfigured(): boolean {
return this.cloudflareClient !== null && this.zoneID !== null;
}
/**
* Add a DNS record for a domain
*/
async addDNSRecord(domain: string, ip?: string): Promise<void> {
try {
if (!this.isConfigured()) {
throw new Error('DNS manager not configured');
}
logger.info(`Adding DNS record for ${domain}`);
const targetIP = ip || this.serverIP;
if (!targetIP) {
throw new Error('Server IP not configured. Set with: onebox config set serverIP <ip>');
}
// Check if record already exists
const existing = await this.getDNSRecord(domain);
if (existing) {
logger.info(`DNS record already exists for ${domain}`);
return;
}
// Create A record
const response = await this.cloudflareClient!.zones.dns.records.create(this.zoneID!, {
type: 'A',
name: domain,
content: targetIP,
ttl: 1, // Auto
proxied: false, // Don't proxy through Cloudflare for direct SSL
});
// Store in database
await this.database.query(
'INSERT INTO dns_records (domain, type, value, cloudflare_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[domain, 'A', targetIP, response.result.id, Date.now(), Date.now()]
);
logger.success(`DNS record created for ${domain}${targetIP}`);
} catch (error) {
logger.error(`Failed to add DNS record for ${domain}: ${error.message}`);
throw error;
}
}
/**
* Remove a DNS record
*/
async removeDNSRecord(domain: string): Promise<void> {
try {
if (!this.isConfigured()) {
throw new Error('DNS manager not configured');
}
logger.info(`Removing DNS record for ${domain}`);
// Get record from database
const rows = this.database.query('SELECT cloudflare_id FROM dns_records WHERE domain = ?', [
domain,
]);
if (rows.length === 0) {
logger.warn(`DNS record not found for ${domain}`);
return;
}
const cloudflareID = String(rows[0][0]);
// Delete from Cloudflare
if (cloudflareID) {
await this.cloudflareClient!.zones.dns.records.delete(this.zoneID!, cloudflareID);
}
// Delete from database
this.database.query('DELETE FROM dns_records WHERE domain = ?', [domain]);
logger.success(`DNS record removed for ${domain}`);
} catch (error) {
logger.error(`Failed to remove DNS record for ${domain}: ${error.message}`);
throw error;
}
}
/**
* Get DNS record for a domain
*/
async getDNSRecord(domain: string): Promise<any> {
try {
if (!this.isConfigured()) {
return null;
}
// Get from database first
const rows = this.database.query('SELECT * FROM dns_records WHERE domain = ?', [domain]);
if (rows.length > 0) {
return {
domain: String(rows[0][1]),
type: String(rows[0][2]),
value: String(rows[0][3]),
cloudflareID: rows[0][4] ? String(rows[0][4]) : null,
};
}
return null;
} catch (error) {
logger.error(`Failed to get DNS record for ${domain}: ${error.message}`);
return null;
}
}
/**
* List all DNS records
*/
listDNSRecords(): any[] {
try {
const rows = this.database.query('SELECT * FROM dns_records ORDER BY created_at DESC');
return rows.map((row) => ({
id: Number(row[0]),
domain: String(row[1]),
type: String(row[2]),
value: String(row[3]),
cloudflareID: row[4] ? String(row[4]) : null,
createdAt: Number(row[5]),
updatedAt: Number(row[6]),
}));
} catch (error) {
logger.error(`Failed to list DNS records: ${error.message}`);
return [];
}
}
/**
* Sync DNS records from Cloudflare
*/
async syncFromCloudflare(): Promise<void> {
try {
if (!this.isConfigured()) {
throw new Error('DNS manager not configured');
}
logger.info('Syncing DNS records from Cloudflare...');
const response = await this.cloudflareClient!.zones.dns.records.list(this.zoneID!);
const records = response.result;
// Only sync A records
const aRecords = records.filter((r: any) => r.type === 'A');
for (const record of aRecords) {
// Check if exists in database
const existing = await this.getDNSRecord(record.name);
if (!existing) {
// Add to database
await this.database.query(
'INSERT INTO dns_records (domain, type, value, cloudflare_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[record.name, record.type, record.content, record.id, Date.now(), Date.now()]
);
logger.info(`Synced DNS record: ${record.name}`);
}
}
logger.success('DNS records synced from Cloudflare');
} catch (error) {
logger.error(`Failed to sync DNS records: ${error.message}`);
throw error;
}
}
/**
* Check if domain DNS is properly configured
*/
async checkDNS(domain: string): Promise<boolean> {
try {
logger.info(`Checking DNS for ${domain}...`);
// Use dig or nslookup to check DNS resolution
const command = new Deno.Command('dig', {
args: ['+short', domain],
stdout: 'piped',
stderr: 'piped',
});
const { code, stdout } = await command.output();
if (code !== 0) {
logger.warn(`DNS check failed for ${domain}`);
return false;
}
const ip = new TextDecoder().decode(stdout).trim();
if (ip === this.serverIP) {
logger.success(`DNS correctly points to ${ip}`);
return true;
} else {
logger.warn(`DNS points to ${ip}, expected ${this.serverIP}`);
return false;
}
} catch (error) {
logger.error(`Failed to check DNS for ${domain}: ${error.message}`);
return false;
}
}
}