Skip to content
acmeproxy.php 7.92 KiB
Newer Older
<?php

require_once '../config/config.inc.php';
require_once 'utils.php';

kobuki's avatar
kobuki committed
class AcmeWrapper
{

    private $sessionId;
    private $client;
    private $clientZones;
    private $clientZoneNames;
    private $username;
    private $config;

kobuki's avatar
kobuki committed
    function __construct($config)
    {
        $this->config = $config;

        $resp = $this->jsonCall('login', [
            'username' => $config['apiUsername'],
            'password' => $config['apiPassword'],
            'client_login' => false]);
        if ($resp->code != 'ok') {
            throw new RuntimeException('API setup error');
        }
        $this->sessionId = $resp->response;
    }

kobuki's avatar
kobuki committed
    public function jsonCall($function, $data = null)
    {
        if (empty($data)) {
            $data = new stdClass();
        } else {
            $data = (object)$data;
        }
        $data->session_id = $this->sessionId;
        return jsonRequest("{$this->config['apiUrl']}?$function", $data);
    }

kobuki's avatar
kobuki committed
    public function initClient($username, $password, $sessionId)
    {
        if (!$username && !$sessionId) {
kobuki's avatar
kobuki committed
            throw new InvalidArgumentException("Invalid credentials");
        }
        if ($sessionId) {
            session_id($sessionId);
            session_start();
            $username = $_SESSION['username'];
            $password = $_SESSION['password'];
        } else {
            session_start();
            $sessionId = session_id();
            $_SESSION['username'] = $username;
            $_SESSION['password'] = $password;
        }
        $this->client = $this->clientZones = $this->clientZoneNames = null;
        $this->username = $username;
        $client = $this->jsonCall('client_get_by_username', ['username' => $username]);
kobuki's avatar
kobuki committed
        if ($client->code != 'ok' || !$this->checkCryptValue($password, $client->response->passwort)) {
            throw new InvalidArgumentException("Invalid credentials");
        }
        $this->client = $client->response;
        $clientZones = $this->jsonCall('dns_zone_get_by_user',
            ['client_id' => $this->client->client_id, 'server_id' => $this->config['dnsServerId']]);
        $this->clientZones = $clientZones->response;
        foreach ($this->clientZones as $clientZone) {
            $this->clientZoneNames[$clientZone->origin] = $clientZone->id;
        }
        return $sessionId;
    }

kobuki's avatar
kobuki committed
    function __destruct()
    {
        if ($this->sessionId) {
            $this->jsonCall('logout');
        }
    }

    /**
     * @param $password
     * @param $saved_password
     * @return bool
     */
kobuki's avatar
kobuki committed
    private static function checkCryptValue($password, $saved_password)
    {
        if ($saved_password[0] == '{') {
            // remove Dovecot-style password prefix (used for email user logins)
            // example: {MD5-CRYPT}$1$12345678$MfjBLH.L2J1K2v0dXHkeJ/
            $saved_password = substr($saved_password, strpos($saved_password, '}') + 1);
        }

kobuki's avatar
kobuki committed
        if ($saved_password[0] == '$') {
            // assume prefixed crypt() hash
            // $saved_password can be used as the salt, as php ignores the part after the last $ character
            return crypt(stripslashes($password), $saved_password) == $saved_password;
        } else {
            // assume MD5 hash
            return md5(stripslashes($password)) == $saved_password;
        }
    }

    /**
     * @param $code
     * @param $message
     * @param string $data
     */
kobuki's avatar
kobuki committed
    private function returnJson($code, $message, $data = '')
    {
        $ret = new stdClass;
        $ret->code = $code;
        $ret->message = $message;
        $ret->response = $data;

        header('Content-Type: application/json; charset="utf-8"');
        echo json_encode($ret);
    }

kobuki's avatar
kobuki committed
    private function proxyCall($method, $data)
    {
        $resp = $this->jsonCall($method, $data);
        $this->returnJson($resp->code, $resp->message, $resp->response);
    }

kobuki's avatar
kobuki committed
    public function incrementZoneSerial($zoneId)
    {
        $soa = $this->jsonCall('dns_zone_get', ['primary_id' => $zoneId])->response;
        $serial = $soa->serial;
        $serial_date = intval(substr($serial, 0, 8));
        $count = intval(substr($serial, 8, 2));
        $current_date = date("Ymd");
        if ($serial_date >= $current_date) {
            $count += 1;
            if ($count > 99) {
                $serial_date += 1;
                $count = 0;
            }
            $count = str_pad($count, 2, "0", STR_PAD_LEFT);
            $new_serial = $serial_date . $count;
        } else {
            $new_serial = $current_date . '01';
        }
        $soa->serial = $new_serial;
        $this->jsonCall('dns_zone_update', ['client_id' => $this->client->client_id, 'primary_id' => $soa->id, 'params' => $soa]);
    }

    public function handleJson()
    {
        if (!isset($_GET) || !is_array($_GET) || count($_GET) < 1) {
            $this->returnJson('invalid_method', 'Method not provided in json call');
            return;
        }
        try {
            $keys = array_keys($_GET);
            $method = reset($keys);
            $raw = file_get_contents("php://input");
            $data = json_decode($raw);
            if (empty($data)) throw new RuntimeException('Invalid JSON data');

            switch ($method) {
                case 'login':
                    $sid = $this->initClient($data->username, $data->password, null);
                    $this->returnJson('ok', '', $sid);
                    break;
                case 'dns_zone_get':
                    $this->initClient(null, null, $data->session_id);
kobuki's avatar
kobuki committed
                    if (!array_key_exists($data->primary_id->origin, $this->clientZoneNames)) {
                        throw new RuntimeException('Permission denied');
                    }
                    $this->proxyCall($method, $data);
                    break;
                case 'dns_txt_add':
                    $this->initClient(null, null, $data->session_id);
                    $fulldomain = $data->params->name;
                    $zoneId = -1;
                    foreach ($this->clientZoneNames as $d => $id) {
                        if (endsWith($fulldomain, $d)) {
                            $zoneId = $id;
                            break;
                        }
                    }
                    if ($data->params->server_id != $this->config['dnsServerId'] || $zoneId < 0) {
                        throw new RuntimeException('Permission denied');
                    }
                    $this->proxyCall($method, $data);
                    $this->incrementZoneSerial($zoneId);
                    break;
                case 'dns_txt_get':
                    $this->initClient(null, null, $data->session_id);
                    $fulldomain = $data->primary_id->name;
                    $zoneId = -1;
                    foreach ($this->clientZoneNames as $d => $id) {
                        if (endsWith($fulldomain, $d)) {
                            $zoneId = $id;
                            break;
                        }
                    }
                    if ($zoneId < 0) {
                        throw new RuntimeException('Permission denied');
                    }
                    $this->proxyCall($method, $data);
                    break;
                case 'dns_txt_delete':
                    $this->initClient(null, null, $data->session_id);
                    $recordId = $data->primary_id;
                    $record = $this->jsonCall('dns_txt_get', ['primary_id' => $recordId])->response;
                    if (empty($record) || array_search($record->zone, $this->clientZoneNames) === false || $record->type != 'TXT') {
                        throw new RuntimeException('Permission denied');
                    }
                    $this->proxyCall($method, $data);
kobuki's avatar
kobuki committed
                    $this->incrementZoneSerial($record->zone);
                    break;
            }
        } catch (Exception $e) {
            $this->returnJson('invalid_data', $e->getMessage());
        }
    }
}

$acme = new AcmeWrapper($config);
$acme->handleJson();