Commit 06032f89 authored by Marius Burkard's avatar Marius Burkard

Merge branch 'master' into 'master'

Master

See merge request !4
parents d9a5bb64 bad4ffeb
### About this module
The experimental ispc_acmeproxy module is an attempt to create an environment running alongside an existing ISPConfig installation and uses normal user login credentials to edit DNS TXT records needed for the ACME DNS domain validation. It's primarily made for the [acme.sh](https://github.com/Neilpang/acme.sh) client. The ISPConfig DNS plugin for acme.sh can be used without changes, using user credentials instead of remoting api credentials that cannot be restricted to user resources. Experimental means it works fine for me where I need it, but probably needs more rigorous testing.
### Suggested usage
1. create an ISPC remoting user with proper rights (Client functions, DNS zone functions, DNS txt functions)
2. checkout the repository and copy the ispc_acmeproxy to a convenient place
3. create a new vhost under your web server and point the root to ispc_acmeproxy/web
4. copy ispc_acmeproxy/config/config.inc.php.sample to ispc_acmeproxy/config/config.inc.php and edit accordingly (use credentials from step 1)
5. reload your web server, test using the acme<span></span>.sh client, etc.
You'll need to provide user credentials instead of the API credentials for the DNS plugin as per [the docs](https://github.com/Neilpang/acme.sh/tree/master/dnsapi).
### Etc.
See this [forum thread](https://www.howtoforge.com/community/threads/restricting-api-users-to-certain-domains-or-limiting-to-one-clients-resources.80450/)
Suggestions, issue reports, PRs are welcome.
<?php
$config = [
'apiUrl' => 'https://ispconfig.example.com:8443/remote/json.php',
'apiUsername' => 'remotingUserName',
'apiPassword' => 'remotingUserPass',
'dnsServerId' => 5
];
<?php
require_once '../config/config.inc.php';
require_once 'utils.php';
class AcmeWrapper
{
private $sessionId;
private $client;
private $clientZones;
private $clientZoneNames;
private $username;
private $config;
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;
}
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();
<?php
/**
* @param string $data
* @param string $key
* @param string $method
* @return string
*/
function encrypt(string $data, string $key, string $method): string
{
$ivSize = openssl_cipher_iv_length($method);
$iv = openssl_random_pseudo_bytes($ivSize);
$encrypted = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv);
// For storage/transmission, we simply concatenate the IV and cipher text
//$encrypted = base64_encode($iv . $encrypted);
$encrypted = bin2hex($iv . $encrypted);
return $encrypted;
}
/**
* @param string $data
* @param string $key
* @param string $method
* @return string
*/
function decrypt(string $data, string $key, string $method): string
{
//$data = base64_decode($data);
$data = hex2bin($data);
$ivSize = openssl_cipher_iv_length($method);
$iv = substr($data, 0, $ivSize);
$data = openssl_decrypt(substr($data, $ivSize), $method, $key, OPENSSL_RAW_DATA, $iv);
return $data;
}
/**
* @param $string
* @param $endString
* @return bool
*/
function endsWith($string, $endString)
{
$len = strlen($endString);
if ($len == 0) {
return true;
}
return (substr($string, -$len) === $endString);
}
function jsonRequest($url, $data)
{
$request = array(
'http' => array(
'method' => 'POST',
'content' => json_encode($data),
'header' =>
"Content-Type: application/json\r\n" .
"Accept: application/json\r\n"
)
);
$context = stream_context_create($request);
$result = file_get_contents($url, false, $context);
$response = json_decode($result);
return $response;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment