Initial commit: Onebox v1.0.0
- Complete Deno-based architecture following nupst/spark patterns - SQLite database with full schema - Docker container management - Service orchestration (Docker + Nginx + DNS + SSL) - Registry authentication - Nginx reverse proxy configuration - Cloudflare DNS integration - Let's Encrypt SSL automation - Background daemon with metrics collection - HTTP API server - Comprehensive CLI - Cross-platform compilation setup - NPM distribution wrapper - Shell installer script Core features: - Deploy containers with single command - Automatic domain configuration - Automatic SSL certificates - Multi-registry support - Metrics and logging - Systemd integration Ready for Angular UI implementation and testing.
This commit is contained in:
270
ts/onebox.classes.dns.ts
Normal file
270
ts/onebox.classes.dns.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* DNS Manager for Onebox
|
||||
*
|
||||
* Manages DNS records via Cloudflare API
|
||||
*/
|
||||
|
||||
import * as plugins from './onebox.plugins.ts';
|
||||
import { logger } from './onebox.logging.ts';
|
||||
import { OneboxDatabase } from './onebox.classes.database.ts';
|
||||
|
||||
export class OneboxDnsManager {
|
||||
private oneboxRef: any;
|
||||
private database: OneboxDatabase;
|
||||
private cloudflareClient: plugins.cloudflare.Cloudflare | 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 zoneID = this.database.getSetting('cloudflareZoneID');
|
||||
const serverIP = this.database.getSetting('serverIP');
|
||||
|
||||
if (!apiKey || !email || !zoneID) {
|
||||
logger.warn('Cloudflare credentials not configured. DNS management will be disabled.');
|
||||
logger.info('Configure with: onebox config set cloudflareAPIKey <key>');
|
||||
return;
|
||||
}
|
||||
|
||||
this.zoneID = zoneID;
|
||||
this.serverIP = serverIP;
|
||||
|
||||
// Initialize Cloudflare client
|
||||
this.cloudflareClient = new plugins.cloudflare.Cloudflare({
|
||||
apiKey,
|
||||
email,
|
||||
});
|
||||
|
||||
logger.info('DNS manager initialized with Cloudflare');
|
||||
} catch (error) {
|
||||
logger.error(`Failed to initialize DNS manager: ${error.message}`);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user