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; } 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); } public function initClient($username, $password, $sessionId) { if (!$username && !$sessionId) { 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]); 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; } function __destruct() { if ($this->sessionId) { $this->jsonCall('logout'); } } /** * @param $password * @param $saved_password * @return bool */ 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); } 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 */ 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); } private function proxyCall($method, $data) { $resp = $this->jsonCall($method, $data); $this->returnJson($resp->code, $resp->message, $resp->response); } 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); 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); $this->incrementZoneSerial($record->zone); break; } } catch (Exception $e) { $this->returnJson('invalid_data', $e->getMessage()); } } } $acme = new AcmeWrapper($config); $acme->handleJson();