Gestire le zone DNS su Cloudflare con PHP e JavaScript

Gestire le zone DNS su Cloudflare con PHP e JavaScript

In questo tutorial vedremo come gestire una zona DNS su Cloudflare con PHP e JavaScript.

Le API di Cloudflare utilizzano come meccanismo di autenticazione un header di tipo Bearer con un token che dobbiamo generare nella nostra area utente.

Una volta creato il token dobbiamo aggiungerlo ad ogni richiesta. Le API di Cloudflare sono di tipo REST, quindi utilizzano il formato JSON sia per le richieste che per le risposte.

Creiamo quindi lo scheletro di una classe PHP dove useremo cURL per effettuare le richieste.

class CloudFlare {
    const BASE_URL = 'https://api.cloudflare.com/client/v4/';
    const TOKEN = '';
    
    public static function deleteDNSRecord()
    {
        $zone_id = $_POST['zone_id'];
        $record_id = $_POST['record_id'];

        $request = self::makeRequest([], 'DELETE', 'zones/' . $zone_id . '/dns_records/' . $record_id);

        $response = [
            'success' => true,
            'data' => $request
        ];

        if(isset($request['error'])) {
            $response['success'] = false;
            $response['data'] = null;
            $response['error'] = $request['error'];
        }

        self::json($response);
    }


    public static function addDNSRecord()
    {
        $zone_id = $_POST['id'];
        $name = $_POST['name'];
        $type = $_POST['type'];
        $content = $_POST['content'];
        $priority = $_POST['priority'];
        $ttl = $_POST['ttl'];

        $data = compact('name', 'type', 'content', 'ttl');

        $request = self::makeRequest($data, 'POST', 'zones/' . $zone_id . '/dns_records');

        $response = [
            'success' => true,
            'data' => $request
        ];

        if(isset($request['error'])) {
            $response['success'] = false;
            $response['data'] = null;
            $response['error'] = $request['error'];
        }

        self::json($response);
    }

    public static function getDNSZoneRecords($zone_id)
    {
        $request = self::makeRequest([], 'GET', 'zones/' . $zone_id . '/dns_records');
        $response = [
            'success' => true,
            'data' => $request
        ];

        if(isset($request['error'])) {
            $response['success'] = false;
            $response['data'] = null;
            $response['error'] = $request['error'];
        }

        self::json($response);
    }

    public static function getDNSZoneDetails($zone_id)
    {
        $request = self::makeRequest([], 'GET', 'zones/' . $zone_id);
        $response = [
            'success' => true,
            'data' => $request
        ];

        if(isset($request['error'])) {
            $response['success'] = false;
            $response['data'] = null;
            $response['error'] = $request['error'];
        }

        self::json($response);
    }

    public static function getDNSZones()
    {
        $request = self::makeRequest([], 'GET', 'zones');
        $response = [
            'success' => true,
            'data' => $request
        ];

        if(isset($request['error'])) {
            $response['success'] = false;
            $response['data'] = null;
            $response['error'] = $request['error'];
        }

        self::json($response);
    }
    
    private static function json($data)
    {
        header('Content-Type: application/json');
        echo json_encode($data);
        exit;
    }

    private static function makeRequest($data = [], $method = 'GET', $endpoint = '')
    {
        $data_string = is_array($data) && count($data) > 0 ? json_encode($data) : '';
        $url = self::BASE_URL . $endpoint;

        $headers = [
            'Content-Type: application/json',
            'Authorization: Bearer ' . self::TOKEN
        ];

        $ch = curl_init($url);

        if($method !== 'GET') {
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
        }

        if(!empty($data_string)) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
            $headers[] = 'Content-Length: ' . strlen($data_string);
        }
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

        $result = curl_exec($ch);

        if($result === false) {
            return ['error' => curl_error($ch)];
        }

        curl_close($ch);

        return json_decode($result, true);
    }

Definiamo quindi un endpoint per le richieste AJAX effettuate dal codice JavaScript che chiameremo a titolo di esempio ajax.php. Il client dovrà fornire un parametro action in base al quale andremo ad invocare un metodo specifico della nostra classe.

require_once 'lib/CloudFlare.php';

$action = $_POST['action'];
$actions = ['dns-zones', 'dns-zone-details', 'dns-zone-records', 'add-dns-record', 'delete-dns-record'];
if(!isset($action) || empty($action) || !in_array($action, $actions)) {
    http_response_code(403);
    echo json_encode(['error' => 403]);
    exit;
}

switch($action) {
    case 'dns-zones':
        CloudFlare::getDNSZones();
        break;
    case 'dns-zone-details':
        CloudFlare::getDNSZoneDetails($_POST['id']);
        break;
    case 'dns-zone-records':
        CloudFlare::getDNSZoneRecords($_POST['id']);
        break;
    case 'add-dns-record':
        CloudFlare::addDNSRecord();
        break;
    case 'delete-dns-record':
        CloudFlare::deleteDNSRecord();
        break;
     default:
        http_response_code(405);
        echo json_encode(['error' => 405]);
        exit;
}

Una zona DNS ha un ID e a sua volta anche un record DNS ha un proprio ID restituiti da Cloudflare insieme al tipo di record, al nome, al contenuto, al TTL e alla priorità (quest'ultima obbligatoria per i record MX).

JavaScript popolerà la seguente struttura HTML con i dati ricevuti dalla nostra classe.

<main id="site">
    <h1>DNS Zones</h1>

    <section id="dns-zones">

    </section>
</main>

<div id="loader" aria-hidden="true">
    <div class="lds-roller"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>
</div>

Definiamo il nostro codice JavaScript partendo dalle funzioni e dalle classi helper.

'use strict';

(function () {

    const serializeFormAsObject = form => {
        const elements = form.querySelectorAll('input, select, textarea');
        let data = {};
        elements.forEach(element => {
            data[element.getAttribute('name')] = element.value;
        });
        return data;
    };

    const isMobile = () => {
        return /mobile/gi.test(navigator.userAgent);
    };

    async function request(url = '', method = 'GET', data = null) {
        const settings = {
            method
        };
        if (method === 'POST') {
            settings.headers = {
                'Content-Type': 'application/x-www-form-urlencoded'
            };
            let body = [];
            for (let prop in data) {
                if (data.hasOwnProperty(prop)) {
                    let part = `${prop}=${data[prop]}`;
                    body.push(part);
                }
            }

            settings.body = body.join('&');
        }

        const response = await fetch(url, settings);
        return response.json();
    }

    class Modal {
        constructor(id) {
            this.id = id;
            this.modal = document.createElement('div');
            this.modal.id = this.id;
            this.modal.className = 'modal';

            this.events();
        }

        init() {
            document.body.appendChild(this.modal);
        }

        events() {
            this.modal.addEventListener('click', e => {
                e.preventDefault();
                const element = e.target;
                if(element.matches('.modal-close')) {
                    this.hide();
                }
            }, false);
        }

        show() {
            document.body.classList.add('overlay');
            this.modal.style.display = 'block';
        }

        hide() {
            this.modal.style.display = 'none';
            document.body.classList.remove('overlay');
        }

        html(str) {
            let close = '<a href="#' + this.id + '" class="modal-close">&times;</a>';
            this.modal.innerHTML = close + str;
        }
    }

    class SelectBox {
        constructor(name, items) {
            this.name = name;
            this.items = items;
        }

        render() {
            let html = '<select name="' + this.name + '">';
            this.items.forEach((item, index) => {
                let selected = index === 0 ? ' selected' : '';
                html += '<option value="' + item + '"' + selected + '>' + item + '</option>';
            });
            html += '</select>';

            return html;
        }
    }
    //...
})();        

serializeFormAsObject() trasforma i campi dei form e i loro valori in un oggetto JavaScript, isMobile() verifica il tipo di dispositivo in uso per ottimizzare la struttura per i dispositivi mobile, request() usa le Fetch API per effettuare le richieste AJAX, la classe Modal crea una finestra modale e la classe SelectBox genera la struttura HTML degli elementi select.

La nostra classe principale viene inizializzata in questo modo:

class Cloudflare {
        constructor() {
            this.url = 'ajax.php';
            this.dnsZones = document.querySelector('#dns-zones');
            this.dnsRecords = 'A,AAAA,CNAME,HTTPS,TXT,SRV,LOC,MX,NS,SPF,CERT,DNSKEY,DS,NAPTR,SMIMEA,SSHFP,SVCB,TLSA,URI'.split(',');
            this.currentZoneID = '';
            this.loader = document.querySelector('#loader');
            this.init();
        }

        init() {
            this.modals();
            this.eventListeners();
            this.getDNSZones().then(data => {
                this.loader.style.display = 'none';
                this.displayDNSZones(data.data.result);
            });
        }

La prima azione della classe è reperire l'elenco delle zone DNS presenti su Cloudflare e quindi mostrarle all'utente. Prima viene invocato il metodo getDNSZones():

getDNSZones() {
            return request(this.url, 'POST', {
                action: 'dns-zones'
            });
        }

Quindi i dati ricevuti (un array di una o più zone) vengono visualizzati tramite il metodo displayDNSZones().

displayDNSZones(data) {
            if(Array.isArray(data) && data.length > 0) {
                let html = '';
                data.forEach(datum => {
                    html += '<div class="dns-zone">';
                    html += '<header><h2>' + datum.name + '</h2></header>';
                    html += '<p><button type="button" data-id="' + datum.id + '" class="button dns-zone-detail">Get details</button> <a href="#" class="dns-zone-add" data-id="' + datum.id + '">Add new record</a></p>';
                    html += isMobile() ? '<div class="dns-zone-details"></div>' : '<table class="dns-zone-details"></table>';
                    html += '</div>';
                });

                this.dnsZones.innerHTML = html;
            }
        }

Per visualizzare i record della zona, dobbiamo legare un evento click al pulsante dns-zone-detail. Allo stesso modo se vogliamo lanciare il modale di aggiunta di un record, dobbiamo legare il medesimo evento al link dns-zone-add. Entrambi i controlli hanno l'ID della zona come attributo di dati. Useremo a tale scopo la event delegation creando un listener globale e indirizzando le nostre azioni verso specifici elementi target del DOM.

eventListeners() {
            document.addEventListener('click', e => {
                e.preventDefault();
                const element = e.target;
                if(element.matches('.dns-zone-detail')) {
                    this.currentZoneID = element.dataset.id;
                    this.getDNSZoneRecords(element, element.dataset.id);
                }
                if(element.matches('.dns-zone-add')) {
                    this.currentZoneID = element.dataset.id;
                    this.dnsRecordModal.show();
                }
                if(element.matches('.dns-record-add-btn')) {
                    this.addDNSRecord(document.querySelector('#dns-record-add-form'));
                }
                if(element.matches('.delete-record')) {
                    this.deleteDNSRecord(element);
                }
            }, false);

            this.dnsRecordModal.modal.addEventListener('submit', e => {
                e.preventDefault();

            }, false);
        }

La proprietà currentZoneID viene impostata dinamicamente in modo da avere sempre l'ID della zona corrente disponibile all'occorrenza.

Per visualizzare i record di una zona avremo:

displayDNSRecords(element, records) {
            if(Array.isArray(records) && records.length > 0) {
                let html = '';
                records.forEach(record => {
                    if(isMobile()) {
                        html += `<div class="dns-zone-row"><div class="dns-zone-record">${record.name}</div><div class="dns-zone-record-content">${record.content}</div><div class="dns-zone-record-type">${record.type}</div><div class="dns-zone-record-delete"><a href="#" class="delete-record" data-id="${record.id}">&times;</a></div></div>`;
                    } else {
                        html += `<tr class="dns-zone-row"><td class="dns-zone-record">${record.name}</td><td class="dns-zone-record-content">${record.content}</td><td class="dns-zone-record-type">${record.type}</td><td class="dns-zone-record-delete"><a href="#" class="delete-record" data-id="${record.id}">&times;</a></td></tr>`;
                    }
                });

                element.parentNode.nextElementSibling.innerHTML = html;
            }
        }

        getDNSZoneRecords(button, zoneID) {
            const self = this;
            self.loader.style.display = 'block';
            request(self.url, 'POST', {
                action: 'dns-zone-records',
                id: zoneID
            }).then(data => {
                self.loader.style.display = 'none';
               self.displayDNSRecords(button, data.data.result);
            });
        }

Per aggiungere un record useremo il seguente metodo:

addDNSRecord(form) {
           let data = serializeFormAsObject(form);
           data.action = 'add-dns-record';
           data.id = this.currentZoneID;

            this.dnsRecordModal.hide();
           this.loader.style.display = 'block';

           request(this.url, 'POST', data).then(res => {

                if(res.success && res.data.success) {
                    const record = res.data.result;
                    let html = '';
                    if(isMobile()) {
                        html += `<div class="dns-zone-row"><div class="dns-zone-record">${record.name}</div><div class="dns-zone-record-content">${record.content}</div><div class="dns-zone-record-type">${record.type}</div><div class="dns-zone-record-delete"><a href="#" class="delete-record" data-id="${record.id}">&times;</a></div></div>`;
                    } else {
                        html += `<tr class="dns-zone-row"><td class="dns-zone-record">${record.name}</td><td class="dns-zone-record-content">${record.content}</td><td class="dns-zone-record-type">${record.type}</td><td class="dns-zone-record-delete"><a href="#" class="delete-record" data-id="${record.id}">&times;</a></td></tr>`;
                    }
                    document.querySelector('.dns-zone-details').insertAdjacentHTML('afterbegin', html);
                } else {
                    console.log(res);
                }

               this.loader.style.display = 'none';



           });

        }

Infine, per eliminare un record dovremo usare sia l'ID della zona che l'ID del record stesso.

deleteDNSRecord(element) {
            const zoneID = this.currentZoneID;
            const data = {
                action: 'delete-dns-record',
                zone_id: zoneID,
                record_id: element.dataset.id
            };

            if(confirm('Do you really want to delete this record?')) {
                this.loader.style.display = 'block';
                request(this.url, 'POST', data).then(result => {
                    if(result.success && result.data.result && result.data.result.id === element.dataset.id) {
                        element.parentNode.parentNode.remove();
                    }
                    this.loader.style.display = 'none';
                });
            }
        }

Conclusione

Gestire una zona DNS di Cloudflare con PHP e JavaScript si rivela essere un compito relativamente semplice data la natura RESTful delle API di Cloudflare.

Torna su