|
const PORT = 3000 |
|
const http = require('http'); |
|
const request = require('request'); |
|
const path = require('path'); |
|
const fs = require('fs'); |
|
const src = fs.readFileSync(path.basename(__filename)); |
|
const html = src.toString().split('/'+'*TEMPLATE')[1].split('TEMPLATE*'+'/')[0]; |
|
const server = http.createServer((req, res) => { |
|
// CORS fix |
|
if (req.method === 'OPTIONS') { |
|
res.writeHead(200, { |
|
'Access-Control-Allow-Origin': '*', |
|
'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE', |
|
'Access-Control-Allow-Headers': 'Authorization, Content-Type', |
|
'Access-Control-Max-Age': '86400' |
|
}); |
|
res.end(); |
|
return; |
|
} |
|
|
|
// Serve template |
|
if (req.url === '/') { |
|
res.writeHead(200, { 'Content-Type': 'text/html' }); |
|
res.end(html); |
|
return; |
|
} |
|
|
|
// Disallow anything not in api |
|
if (!req.url.startsWith('/api/')) { |
|
res.writeHead(404, { 'Content-Type': 'application/json' }); |
|
res.end(JSON.stringify({ success: false, errors: [{ code: 404, message: 'Not found' }] })); |
|
return; |
|
} |
|
|
|
console.log(req.method, req.url); |
|
const apiUrl = `https://api.cloudflare.com/client/v4${req.url.replace('/api', '')}`; |
|
const headers = { 'Authorization': req.headers['authorization'], 'Content-Type': 'application/json' }; |
|
|
|
// Proxy to Cloudflare API |
|
let body = ''; |
|
req.on('data', chunk => body += chunk); |
|
req.on('end', () => { |
|
request({ |
|
method: req.method, |
|
url: apiUrl, |
|
headers, |
|
body: body || undefined, |
|
json: true |
|
}, (error, response, body) => { |
|
console.log(error, body); |
|
res.writeHead(response.statusCode, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); |
|
res.end(JSON.stringify(body)); |
|
}); |
|
}); |
|
}); |
|
|
|
server.listen(PORT, () => console.log('Proxy running on http://localhost:'+PORT)); |
|
|
|
/*TEMPLATE |
|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Cloudflare DNS Manager</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
</head> |
|
<body class="bg-gray-100 font-sans"> |
|
<div class="container mx-auto p-4"> |
|
<h1 class="text-2xl font-bold mb-4">Cloudflare DNS Manager</h1> |
|
<div class="mb-4 flex space-x-2"> |
|
<input id="apiToken" type="password" placeholder="API Token" class="border p-2 rounded w-1/3"> |
|
<button onclick="fetchZones()" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Load Domains</button> |
|
<select id="zoneSelect" onchange="fetchRecords()" class="border p-2 rounded w-1/2" disabled> |
|
<option value="">Select a domain</option> |
|
</select> |
|
<button onclick="clearCache()" class="bg-yellow-500 text-white px-4 py-2 rounded hover:bg-yellow-600">Clear Cache</button> |
|
</div> |
|
<div id="errorBox" class="hidden bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4"></div> |
|
<table id="dnsTable" class="w-full border-collapse hidden"> |
|
<thead> |
|
<tr class="bg-gray-200"> |
|
<th class="border p-2">Type</th> |
|
<th class="border p-2">Name</th> |
|
<th class="border p-2">Content</th> |
|
<th class="border p-2" style="width: 100px;">Priority</th> |
|
<th class="border p-2" style="width: 100px;">TTL</th> |
|
<th class="border p-2" style="width: 60px;">Proxy</th> |
|
<th class="border p-2">Actions</th> |
|
</tr> |
|
</thead> |
|
<tbody id="dnsTableBody"></tbody> |
|
</table> |
|
</div> |
|
|
|
<script> |
|
const recordTypes = ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'SRV']; |
|
const proxyableTypes = ['A', 'AAAA', 'CNAME']; |
|
const proxyUrl = '/api/'; |
|
let currentDomain = ''; |
|
|
|
function showError(error) { |
|
const errorBox = document.getElementById('errorBox'); |
|
errorBox.innerHTML = `Error ${error.code}: ${error.message}`; |
|
errorBox.classList.remove('hidden'); |
|
setTimeout(() => errorBox.classList.add('hidden'), 5000); |
|
} |
|
|
|
async function fetchZones() { |
|
const apiToken = document.getElementById('apiToken').value; |
|
if (!apiToken) { |
|
showError({ code: 0, message: 'Please enter API Token' }); |
|
return; |
|
} |
|
|
|
try { |
|
const response = await fetch(`${proxyUrl}zones`, { |
|
headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' } |
|
}); |
|
const data = await response.json(); |
|
if (!data.success) { |
|
showError(data.errors[0]); |
|
return; |
|
} |
|
populateZoneSelect(data.result); |
|
} catch (error) { |
|
showError({ code: 0, message: 'Network error: ' + error.message }); |
|
} |
|
} |
|
|
|
function populateZoneSelect(zones) { |
|
const zoneSelect = document.getElementById('zoneSelect'); |
|
zoneSelect.innerHTML = '<option value="">Select a domain</option>'; |
|
zones.forEach(zone => { |
|
const option = document.createElement('option'); |
|
option.value = zone.id; |
|
option.textContent = zone.name; |
|
option.dataset.domain = zone.name; |
|
zoneSelect.appendChild(option); |
|
}); |
|
zoneSelect.disabled = false; |
|
} |
|
|
|
async function fetchRecords() { |
|
const apiToken = document.getElementById('apiToken').value; |
|
const zoneSelect = document.getElementById('zoneSelect'); |
|
const zoneId = zoneSelect.value; |
|
currentDomain = zoneSelect.options[zoneSelect.selectedIndex]?.dataset.domain || ''; |
|
if (!zoneId) { |
|
document.getElementById('dnsTable').classList.add('hidden'); |
|
return; |
|
} |
|
|
|
try { |
|
const response = await fetch(`${proxyUrl}zones/${zoneId}/dns_records`, { |
|
headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' } |
|
}); |
|
const data = await response.json(); |
|
if (!data.success) { |
|
showError(data.errors[0]); |
|
return; |
|
} |
|
renderTable(data.result); |
|
} catch (error) { |
|
showError({ code: 0, message: 'Network error: ' + error.message }); |
|
} |
|
} |
|
|
|
async function clearCache() { |
|
const apiToken = document.getElementById('apiToken').value; |
|
const zoneId = document.getElementById('zoneSelect').value; |
|
if (!zoneId) { |
|
showError({ code: 0, message: 'Please select a domain' }); |
|
return; |
|
} |
|
|
|
try { |
|
const response = await fetch(`${proxyUrl}zones/${zoneId}/purge_cache`, { |
|
method: 'POST', |
|
headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' }, |
|
body: JSON.stringify({ purge_everything: true }) |
|
}); |
|
const data = await response.json(); |
|
if (!data.success) { |
|
showError(data.errors[0]); |
|
return; |
|
} |
|
showError({ code: 200, message: 'Cache cleared successfully' }); |
|
} catch (error) { |
|
showError({ code: 0, message: 'Network error: ' + error.message }); |
|
} |
|
} |
|
|
|
function renderTable(records) { |
|
const tbody = document.getElementById('dnsTableBody'); |
|
tbody.innerHTML = ''; |
|
document.getElementById('dnsTable').classList.remove('hidden'); |
|
|
|
records.forEach(record => { |
|
const row = createRow(record); |
|
tbody.appendChild(row); |
|
}); |
|
|
|
const newRow = createRow({ id: '', type: 'A', name: '', content: '', priority: '', ttl: 1, proxied: false }); |
|
newRow.dataset.newRecord = 'true'; |
|
tbody.appendChild(newRow); |
|
} |
|
|
|
function createRow(record) { |
|
const name = record.name === currentDomain ? '@' : record.name.replace(`.${currentDomain}`, ''); |
|
const priority = record.priority != null ? record.priority : ''; |
|
const row = document.createElement('tr'); |
|
row.innerHTML = ` |
|
<td class="border p-2"> |
|
<select class="border p-1 w-full" onchange="toggleProxyAndPriority(this)"> |
|
${recordTypes.map(type => `<option value="${type}" ${record.type === type ? 'selected' : ''}>${type}</option>`).join('')} |
|
</select> |
|
</td> |
|
<td class="border p-2"><input type="text" value="${name}" class="border p-1 w-full"></td> |
|
<td class="border p-2"><input type="text" value='${record.content || ''}' class="border p-1 w-full"></td> |
|
<td class="border p-2"><input type="number" value="${priority}" min="0" max="65535" class="border p-1 w-full priority-input" ${record.type === 'MX' ? '' : 'disabled'}></td> |
|
<td class="border p-2"><input type="number" value="${record.ttl || 3600}" min="1" class="border p-1 w-full"></td> |
|
<td class="border p-2 text-center"> |
|
<input type="checkbox" ${record.proxied ? 'checked' : ''} ${proxyableTypes.includes(record.type) ? '' : 'disabled'} class="proxy-checkbox"> |
|
</td> |
|
<td class="border p-2"> |
|
<button onclick="saveRecord(this)" class="bg-green-500 text-white px-2 py-1 rounded hover:bg-green-600">Save</button> |
|
${record.id ? `<button onclick="deleteRecord(this, '${record.id}')" class="bg-red-500 text-white px-2 py-1 rounded hover:bg-red-600 ml-2">Delete</button>` : ''} |
|
</td> |
|
`; |
|
row.dataset.id = record.id; |
|
return row; |
|
} |
|
|
|
function toggleProxyAndPriority(select) { |
|
const row = select.closest('tr'); |
|
const proxyCheckbox = row.querySelector('.proxy-checkbox'); |
|
const priorityInput = row.querySelector('.priority-input'); |
|
proxyCheckbox.disabled = !proxyableTypes.includes(select.value); |
|
priorityInput.disabled = select.value !== 'MX'; |
|
if (proxyCheckbox.disabled) proxyCheckbox.checked = false; |
|
if (priorityInput.disabled) priorityInput.value = ''; |
|
} |
|
|
|
async function saveRecord(button) { |
|
const row = button.closest('tr'); |
|
const apiToken = document.getElementById('apiToken').value; |
|
const zoneId = document.getElementById('zoneSelect').value; |
|
const isNew = row.dataset.newRecord === 'true'; |
|
const recordId = row.dataset.id; |
|
const type = row.querySelector('select').value; |
|
let name = row.querySelector('input[type="text"]').value; |
|
const content = row.querySelectorAll('input[type="text"]')[1].value; |
|
const priority = parseInt(row.querySelector('.priority-input').value) || undefined; |
|
const ttl = parseInt(row.querySelectorAll('input[type="number"]')[1].value); |
|
const proxied = row.querySelector('.proxy-checkbox').checked; |
|
|
|
// Append domain to name, convert '@' to domain |
|
name = name === '@' ? currentDomain : name ? `${name}.${currentDomain}` : currentDomain; |
|
|
|
try { |
|
const url = `${proxyUrl}zones/${zoneId}/dns_records${isNew ? '' : `/${recordId}`}`; |
|
const method = isNew ? 'POST' : 'PATCH'; |
|
const body = JSON.stringify({ type, name, content, ttl, proxied, ...(type === 'MX' && priority ? { priority } : {}) }); |
|
|
|
const response = await fetch(url, { |
|
method, |
|
headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' }, |
|
body |
|
}); |
|
const data = await response.json(); |
|
if (!data.success) { |
|
showError(data.errors[0]); |
|
return; |
|
} |
|
fetchRecords(); |
|
} catch (error) { |
|
showError({ code: 0, message: 'Network error: ' + error.message }); |
|
} |
|
} |
|
|
|
async function deleteRecord(button, recordId) { |
|
const apiToken = document.getElementById('apiToken').value; |
|
const zoneId = document.getElementById('zoneSelect').value; |
|
|
|
try { |
|
const response = await fetch(`${proxyUrl}zones/${zoneId}/dns_records/${recordId}`, { |
|
method: 'DELETE', |
|
headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' } |
|
}); |
|
const data = await response.json(); |
|
if (!data.success) { |
|
showError(data.errors[0]); |
|
return; |
|
} |
|
fetchRecords(); |
|
} catch (error) { |
|
showError({ code: 0, message: 'Network error: ' + error.message }); |
|
} |
|
} |
|
</script> |
|
</body> |
|
</html> |
|
TEMPLATE*/ |