Files
ovh-dns/lib/ovh-service.js
2025-10-27 16:14:35 +01:00

438 líneas
16 KiB
JavaScript

import ovh from '@ovhcloud/node-ovh';
import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
const configPath = join(process.cwd(), 'config.json');
export class OVHService {
constructor() {
this.clients = new Map();
this.loadConfig();
}
loadConfig() {
try {
const config = JSON.parse(readFileSync(configPath, 'utf8'));
this.config = config;
// Initialize OVH clients for each account
config.ovhAccounts.forEach(account => {
if (account.appKey && account.appSecret && account.consumerKey) {
this.clients.set(account.id, ovh({
appKey: account.appKey,
appSecret: account.appSecret,
consumerKey: account.consumerKey,
endpoint: account.endpoint || 'ovh-eu'
}));
}
});
} catch (error) {
console.error('Error loading config:', error);
this.config = { ovhAccounts: [], ipProviders: [], autoUpdate: { enabled: false }, currentIPs: {} };
}
}
getClientForDomain(domain) {
// Find which account manages this domain
for (const account of this.config.ovhAccounts) {
if (account.domains && account.domains.includes(domain)) {
return this.clients.get(account.id);
}
}
// Return first available client as fallback
return this.clients.values().next().value;
}
async getAllDomains() {
const allDomains = [];
for (const [accountId, client] of this.clients.entries()) {
try {
const domains = await client.requestPromised('GET', '/domain/zone');
const account = this.config.ovhAccounts.find(acc => acc.id === accountId);
allDomains.push(...domains.map(domain => ({
domain,
accountId,
accountName: account?.name || accountId
})));
} catch (error) {
console.error(`Error fetching domains from account ${accountId}:`, error);
}
}
return allDomains;
}
async getDNSRecords(zoneName) {
try {
const client = this.getClientForDomain(zoneName);
if (!client) {
throw new Error('No OVH client configured for this domain');
}
const recordIds = await client.requestPromised('GET', `/domain/zone/${zoneName}/record`);
const records = await Promise.all(
recordIds.map(async (id) => {
const record = await client.requestPromised('GET', `/domain/zone/${zoneName}/record/${id}`);
return { ...record, id };
})
);
return records;
} catch (error) {
console.error(`Failed to fetch DNS records for ${zoneName}:`, error);
throw error;
}
}
async createDNSRecord(zoneName, recordData) {
try {
const client = this.getClientForDomain(zoneName);
if (!client) {
throw new Error('No OVH client configured for this domain');
}
const record = await client.requestPromised('POST', `/domain/zone/${zoneName}/record`, recordData);
await this.refreshZone(zoneName);
return record;
} catch (error) {
console.error(`Failed to create DNS record in ${zoneName}:`, error);
throw error;
}
}
async updateDNSRecord(zoneName, recordId, recordData) {
try {
const client = this.getClientForDomain(zoneName);
if (!client) {
throw new Error('No OVH client configured for this domain');
}
const record = await client.requestPromised('PUT', `/domain/zone/${zoneName}/record/${recordId}`, recordData);
await this.refreshZone(zoneName);
return record;
} catch (error) {
console.error(`Failed to update DNS record ${recordId} in ${zoneName}:`, error);
throw error;
}
}
async deleteDNSRecord(zoneName, recordId) {
try {
const client = this.getClientForDomain(zoneName);
if (!client) {
throw new Error('No OVH client configured for this domain');
}
await client.requestPromised('DELETE', `/domain/zone/${zoneName}/record/${recordId}`);
await this.refreshZone(zoneName);
} catch (error) {
console.error(`Failed to delete DNS record ${recordId} from ${zoneName}:`, error);
throw error;
}
}
async refreshZone(domain) {
try {
const client = this.getClientForDomain(domain);
if (!client) {
throw new Error('No OVH client configured for this domain');
}
const result = await client.requestPromised('POST', `/domain/zone/${domain}/refresh`);
return result;
} catch (error) {
console.error(`Failed to refresh DNS zone: ${domain}`, error);
throw error;
}
}
async bulkUpdateRecords(zoneName, recordIds, updateData) {
const results = [];
for (const recordId of recordIds) {
try {
const result = await this.updateDNSRecord(zoneName, recordId, updateData);
results.push({ recordId, success: true, result });
} catch (error) {
results.push({ recordId, success: false, error: error.message });
}
}
return results;
}
async bulkDeleteRecords(zoneName, recordIds) {
const results = [];
for (const recordId of recordIds) {
try {
await this.deleteDNSRecord(zoneName, recordId);
results.push({ recordId, success: true });
} catch (error) {
results.push({ recordId, success: false, error: error.message });
}
}
return results;
}
exportToBind9(zoneName, records) {
const lines = [];
lines.push(`; Zone file for ${zoneName}`);
lines.push(`; Generated on ${new Date().toISOString()}`);
lines.push('');
lines.push(`$ORIGIN ${zoneName}.`);
lines.push('');
// Sort records by type for better readability
const sortedRecords = [...records].sort((a, b) => {
const typeOrder = { SOA: 0, NS: 1, A: 2, AAAA: 3, CNAME: 4, MX: 5, TXT: 6, SPF: 7, SRV: 8 };
return (typeOrder[a.fieldType] || 99) - (typeOrder[b.fieldType] || 99);
});
for (const record of sortedRecords) {
const subdomain = record.subDomain || '@';
const ttl = record.ttl || 3600;
let line = '';
switch (record.fieldType) {
case 'A':
case 'AAAA':
line = `${subdomain}\t${ttl}\tIN\t${record.fieldType}\t${record.target}`;
break;
case 'CNAME':
line = `${subdomain}\t${ttl}\tIN\tCNAME\t${record.target}${record.target.endsWith('.') ? '' : '.'}`;
break;
case 'MX':
const priority = record.priority || 10;
line = `${subdomain}\t${ttl}\tIN\tMX\t${priority} ${record.target}${record.target.endsWith('.') ? '' : '.'}`;
break;
case 'TXT':
const txtValue = record.target.includes(' ') && !record.target.startsWith('"')
? `"${record.target}"`
: record.target;
line = `${subdomain}\t${ttl}\tIN\tTXT\t${txtValue}`;
break;
case 'SPF':
const spfValue = record.target.includes(' ') && !record.target.startsWith('"')
? `"${record.target}"`
: record.target;
line = `${subdomain}\t${ttl}\tIN\tSPF\t${spfValue}`;
break;
case 'SRV':
const priority_srv = record.priority || 0;
const weight = record.weight || 0;
const port = record.port || 0;
line = `${subdomain}\t${ttl}\tIN\tSRV\t${priority_srv} ${weight} ${port} ${record.target}${record.target.endsWith('.') ? '' : '.'}`;
break;
case 'NS':
line = `${subdomain}\t${ttl}\tIN\tNS\t${record.target}${record.target.endsWith('.') ? '' : '.'}`;
break;
case 'CAA':
const flags = record.flags || 0;
const tag = record.tag || 'issue';
line = `${subdomain}\t${ttl}\tIN\tCAA\t${flags} ${tag} "${record.target}"`;
break;
default:
line = `${subdomain}\t${ttl}\tIN\t${record.fieldType}\t${record.target}`;
}
lines.push(line);
}
lines.push('');
return lines.join('\n');
}
parseFromBind9(zoneName, zoneContent) {
const records = [];
const lines = zoneContent.split('\n');
let currentOrigin = zoneName;
for (let line of lines) {
// Remove comments
const commentIndex = line.indexOf(';');
if (commentIndex !== -1) {
line = line.substring(0, commentIndex);
}
line = line.trim();
// Skip empty lines
if (!line) continue;
// Handle $ORIGIN directive
if (line.startsWith('$ORIGIN')) {
const parts = line.split(/\s+/);
if (parts[1]) {
currentOrigin = parts[1].replace(/\.$/, '');
}
continue;
}
// Skip $TTL and other directives for now
if (line.startsWith('$')) continue;
// Parse record line
const parts = line.split(/\s+/);
if (parts.length < 4) continue;
let idx = 0;
let subdomain = parts[idx++];
// Handle @ symbol
if (subdomain === '@') {
subdomain = '';
}
// Remove trailing dot from subdomain
if (subdomain.endsWith('.')) {
subdomain = subdomain.slice(0, -1);
}
let ttl = 3600;
let recordClass = 'IN';
let recordType = '';
// Parse TTL (if it's a number)
if (!isNaN(parts[idx])) {
ttl = parseInt(parts[idx++]);
}
// Parse class (usually IN)
if (parts[idx] === 'IN' || parts[idx] === 'CH' || parts[idx] === 'HS') {
recordClass = parts[idx++];
}
// Parse record type
recordType = parts[idx++];
// Parse record data based on type
const recordData = {
fieldType: recordType,
subDomain: subdomain,
ttl: ttl
};
switch (recordType) {
case 'A':
case 'AAAA':
recordData.target = parts[idx];
break;
case 'CNAME':
case 'NS':
recordData.target = parts[idx].replace(/\.$/, '');
break;
case 'MX':
recordData.priority = parseInt(parts[idx++]);
recordData.target = parts[idx].replace(/\.$/, '');
break;
case 'TXT':
// Join remaining parts and remove quotes
const txtValue = parts.slice(idx).join(' ');
recordData.target = txtValue.replace(/^"|"$/g, '');
break;
case 'SPF':
// Join remaining parts and remove quotes
const spfValue = parts.slice(idx).join(' ');
recordData.target = spfValue.replace(/^"|"$/g, '');
break;
case 'SRV':
recordData.priority = parseInt(parts[idx++]);
recordData.weight = parseInt(parts[idx++]);
recordData.port = parseInt(parts[idx++]);
recordData.target = parts[idx].replace(/\.$/, '');
break;
case 'CAA':
recordData.flags = parseInt(parts[idx++]);
recordData.tag = parts[idx++];
const caaValue = parts.slice(idx).join(' ');
recordData.target = caaValue.replace(/^"|"$/g, '');
break;
default:
recordData.target = parts.slice(idx).join(' ');
}
// Skip SOA records as they are managed by OVH
if (recordType !== 'SOA') {
records.push(recordData);
}
}
return records;
}
async importFromBind9(zoneName, zoneContent, replaceAll = false) {
const records = this.parseFromBind9(zoneName, zoneContent);
if (replaceAll) {
// Delete all existing records (except SOA and NS for zone apex)
const existingRecords = await this.getDNSRecords(zoneName);
const recordsToDelete = existingRecords.filter(r =>
r.fieldType !== 'SOA' &&
!(r.fieldType === 'NS' && (!r.subDomain || r.subDomain === ''))
);
for (const record of recordsToDelete) {
try {
await this.deleteDNSRecord(zoneName, record.id);
} catch (error) {
console.error(`Failed to delete record ${record.id}:`, error);
}
}
}
// Create all imported records
const results = [];
for (const recordData of records) {
try {
const result = await this.createDNSRecord(zoneName, recordData);
results.push({ success: true, record: result });
} catch (error) {
results.push({ success: false, error: error.message, record: recordData });
}
}
return results;
}
getConfig() {
return this.config;
}
saveConfig(newConfig) {
try {
writeFileSync(configPath, JSON.stringify(newConfig, null, 2), 'utf8');
this.config = newConfig;
// Reinitialize clients
this.clients.clear();
newConfig.ovhAccounts.forEach(account => {
if (account.appKey && account.appSecret && account.consumerKey) {
this.clients.set(account.id, ovh({
appKey: account.appKey,
appSecret: account.appSecret,
consumerKey: account.consumerKey,
endpoint: account.endpoint || 'ovh-eu'
}));
}
});
return true;
} catch (error) {
console.error('Error saving config:', error);
throw error;
}
}
}
const ovhServiceInstance = new OVHService();
export default ovhServiceInstance;