Compare commits

..

No commits in common. "master" and "v0.9" have entirely different histories.
master ... v0.9

19 changed files with 683 additions and 1917 deletions

3
.gitignore vendored
View file

@ -1,4 +1 @@
includes/config.inc.php
nsedit.sublime*
etc
templates.d/*.json

View file

@ -1,28 +1,38 @@
FROM debian:bookworm
FROM debian:jessie
MAINTAINER Yury Evtikhov <yury@evtikhov.info>
#
# This Dockerfile is intended only for test/development use.
# It will be a really BAD idea to use it for production or public services.
#
#
ENV DEBIAN_FRONTEND noninteractive
#
# Please set the following variables before building:
#
ENV PDNSAPIPWD mypowerdnsapipassword
ENV PDNSAPIIP 192.168.1.2
ENV PDNSAPIPORT 8081
# Update and Upgrade system
RUN apt-get -y update && \
apt-get -y install curl git-core php8.2-cli php8.2-curl php8.2-sqlite3 && \
rm -rf /var/lib/apt/lists/*
RUN mkdir /app
RUN git clone --recursive https://github.com/tuxis-ie/nsedit.git /app/nsedit
RUN cp /app/nsedit/includes/config.inc.php-dist /app/nsedit/includes/config.inc.php
COPY docker-entrypoint.sh /app/nsedit/docker-entrypoint.sh
RUN chmod +x /app/nsedit/docker-entrypoint.sh
apt-get -y install curl git-core php5-cli php5-curl php5-json php5-sqlite && \
mkdir /app && \
git clone --recursive https://github.com/tuxis-ie/nsedit.git /app/nsedit && \
cp /app/nsedit/includes/config.inc.php-dist /app/nsedit/includes/config.inc.php && \
sed "s/\$apipass = ''/\$apipass = '$PDNSAPIPWD'/" -i /app/nsedit/includes/config.inc.php && \
sed "s/\$apiip = ''/\$apiip = '$PDNSAPIIP'/" -i /app/nsedit/includes/config.inc.php && \
sed "s/\$apiport = ''/\$apiport = '$PDNSAPIPORT'/" -i /app/nsedit/includes/config.inc.php && \
sed "s/\$authdb = \"\.\.\/etc\/pdns\.users\.sqlite3\"/\$authdb = \"\/app\/pdns\.users\.sqlite3\"/" -i /app/nsedit/includes/config.inc.php
# Define working directory.
VOLUME /app/nsedit
WORKDIR /app/nsedit
EXPOSE 8080
CMD ["sh", "-c", "/app/nsedit/docker-entrypoint.sh"]
ENTRYPOINT ["/usr/bin/php", "-S", "0.0.0.0:8080"]
#
# Usage:

View file

@ -10,16 +10,14 @@ Features
========
* Import BIND- or AXFR-style dumps of your existing zones
* Add/remove zones and records
* Clone zones
* Show the DNSsec details of a zone
* Multiple user support
* Allow logging of all actions in NSEdit, including exporting the log in JSON-format
* Multiple user support
* [experimental] nsedit API, to create zones from another system
User support
============
Multiple users are supported. A user can be an admin or a normal user. You can
configure whether or not a normal user is allowed to add new zones.
configure wheter or not a normal user is allowed to add new zones.
WeFact Login support
====================
@ -34,7 +32,7 @@ Requirements
* php sqlite3
* php curl
* php with openssl support
* PowerDNS with the JSON-api enabled. Version 4.0.0 or greater
* PowerDNS with the experimental JSON-api enabled (3.4.0 should do. For Pdns > 4.0.0 you ***NEED*** v1.0 of NSEdit)
Installing
==========
@ -43,42 +41,19 @@ Installing
- Run git clone in the directory where you want to run nsedit from
: ```git clone https://github.com/tuxis-ie/nsedit.git```
- Select tag v1.0 or skip this if you want to run from master
: ```git checkout tags/v1.0```
- Select tag v0.9 or skip this if you want to run from master
: ```git checkout tags/v0.9```
* Via releases
- Download the zip-file from [Releases](https://github.com/tuxis-ie/nsedit/releases)
* Copy ```includes/config.inc.php-dist``` to ```includes/config.inc.php``` and edit config.inc.php to your needs.
* By default, nsedit writes its user database to ../etc/pdns.users.sqlite3. Be sure that your webserver can create that directory and write to it. **Make sure the Webserver doesn't serve this file/folder to the public!**
* By default, nsedit writes its user database to ../etc/pdns.users.sqlite3. Be sure that your webserver can create that directory and write to it.
* Visit http(s)://<url>/nsedit/ and login with admin/admin (Don't forget to update your password!)
Have fun ;)
Other methods of installation (Unsupported)
===========================================
* Baji Zsolt created a Suse image: https://susestudio.com/a/vvnMqa/powerdns-with-nsedit
* Yury Evtikhov created the Docker file: https://github.com/tuxis-ie/nsedit/blob/master/Dockerfile
Configuring PowerDNS
====================
Minimal configuration of PowerDNS for supporting nsedit has to include 3 directives:
```
webserver=yes
api=yes
api-key=SomeRandomString
```
Special note for Ubuntu Xenial Xerus 16.04 users:
Default `pdns` package included in Ubuntu repositories has the version of 4.0.0-alpha2 and *nsedit v1.0* doesn't work with it due to API incompatibility.
If your PowerDNS version is not the latest one, please consider adding PowerDNS repository to your system.
Detailed instructions for adding repository are available at http://repo.powerdns.com/
Screenshots
===========

View file

@ -15,7 +15,6 @@ body, html {
border: 1px solid #DDD;
font-family: 'Segoe UI Semilight','Open Sans',Verdana,Arial,Helvetica,sans-serif;
font-weight: 300;
float: left;
font-size: 14px;
line-height: 1.3;
width: 10%;

View file

@ -1,13 +0,0 @@
#!/usr/bin/env bash
[ -z "$PDNSAPIIP" ] && echo "Set PDNSAPIIP to your PowerDNS API IP/Hostname" && exit 1;
[ -z "$PDNSAPIPWD" ] && echo "Set PDNSAPIPWD to your PowerDNS API Password" && exit 1;
sed "s/\$apipass = ''/\$apipass = '$PDNSAPIPWD'/" -i /app/nsedit/includes/config.inc.php
sed "s/\$apiip = ''/\$apiip = '$PDNSAPIIP'/" -i /app/nsedit/includes/config.inc.php
if [[ $PDNSAPIPORT && ${PDNSAPIPORT-x} ]]
then
sed "s/\$apiport = '8081'/\$apiport = '$PDNSAPIPORT'/" -i /app/nsedit/includes/config.inc.php
fi
sed "s/\$authdb = \"\.\.\/etc\/pdns\.users\.sqlite3\"/\$authdb = \"\/app\/pdns\.users\.sqlite3\"/" -i /app/nsedit/includes/config.inc.php
exec /usr/bin/php -S 0.0.0.0:8080

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 B

View file

@ -1,135 +0,0 @@
<?php
include_once('includes/config.inc.php');
class ApiHandler {
public $headers;
public $hostname;
public $port;
public $auth;
public $proto;
public $sslverify;
public $curlh;
public $method;
public $content;
public $apiurl;
public $url;
public $json;
public function __construct() {
global $apiip, $apiport, $apipass, $apiproto, $apisslverify;
$this->headers = Array();
$this->hostname = $apiip;
$this->port = $apiport;
$this->auth = $apipass;
$this->proto = $apiproto;
$this->sslverify = $apisslverify;
$this->curlh = curl_init();
$this->method = 'GET';
$this->content = FALSE;
$this->apiurl = '';
}
public function addheader($field, $content) {
$this->headers[$field] = $content;
}
private function authheaders() {
$this->addheader('X-API-Key', $this->auth);
}
private function apiurl() {
$tmp = new ApiHandler();
$tmp->url = '/api/v1/servers/localhost';
$tmp->go();
$this->apiurl = $tmp->json["url"];
}
private function curlopts() {
$this->authheaders();
$this->addheader('Accept', 'application/json');
if(defined('curl_reset')) {
curl_reset($this->curlh);
} else {
$this->curlh = curl_init();
}
curl_setopt($this->curlh, CURLOPT_HTTPHEADER, Array());
curl_setopt($this->curlh, CURLOPT_RETURNTRANSFER, 1);
if (strcasecmp($this->proto, 'https')) {
curl_setopt($this->curlh, CURLOPT_SSL_VERIFYPEER, $this->sslverify);
}
$setheaders = Array();
foreach ($this->headers as $k => $v) {
array_push($setheaders, join(": ", Array($k, $v)));
}
curl_setopt($this->curlh, CURLOPT_HTTPHEADER, $setheaders);
}
private function baseurl() {
$ip = $this->hostname;
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$ip = sprintf('[%s]', $ip); // curl needs brackets for IPv6
}
return $this->proto.'://'.$ip.':'.$this->port.$this->apiurl;
}
private function go() {
$this->curlopts();
if ($this->content) {
$this->addheader('Content-Type', 'application/json');
curl_setopt($this->curlh, CURLOPT_POST, 1);
curl_setopt($this->curlh, CURLOPT_POSTFIELDS, $this->content);
}
switch ($this->method) {
case 'POST':
curl_setopt($this->curlh, CURLOPT_POST, 1);
break;
case 'GET':
curl_setopt($this->curlh, CURLOPT_POST, 0);
break;
case 'DELETE':
case 'PATCH':
case 'PUT':
curl_setopt($this->curlh, CURLOPT_CUSTOMREQUEST, $this->method);
break;
}
curl_setopt($this->curlh, CURLOPT_URL, $this->baseurl().$this->url);
$return = curl_exec($this->curlh);
$code = curl_getinfo($this->curlh, CURLINFO_HTTP_CODE);
$json = json_decode($return, 1);
if (isset($json['error'])) {
throw new Exception("API Error $code: ".$json['error']);
} elseif ($code < 200 || $code >= 300) {
if ($code == 401) {
throw new Exception("Authentication failed. Have you configured your authmethod correct?");
}
throw new Exception("Curl Error: $code ".curl_error($this->curlh));
}
$this->json = $json;
}
public function call() {
if (substr($this->url, 0, 1) != '/') {
$this->url = '/'.$this->url;
}
$this->apiurl();
$this->url = str_replace($this->apiurl, '', $this->url);
$this->go();
}
}

View file

@ -1,128 +0,0 @@
<?php
include_once('ApiHandler.php');
class PdnsAPI {
public $http;
public function __construct() {
$this->http = new ApiHandler();
}
public function listzones($q = FALSE) {
$api = clone $this->http;
$api->method = 'GET';
if ($q) {
$api->url = "/search-data?q=*".$q."*&max=25";
$api->call();
$ret = Array();
$seen = Array();
foreach ($api->json as $result) {
if (isset($seen[$result['zone_id']])) {
continue;
}
$zone = $this->loadzone($result['zone_id']);
unset($zone['rrsets']);
array_push($ret, $zone);
$seen[$result['zone_id']] = 1;
}
return $ret;
}
$api->url = "/zones";
$api->call();
return $api->json;
}
public function loadzone($zoneid) {
$api = clone $this->http;
$api->method = 'GET';
$api->url = "/zones/$zoneid";
$api->call();
return $api->json;
}
public function exportzone($zoneid) {
$api = clone $this->http;
$api->method = 'GET';
$api->url = "/zones/$zoneid/export";
$api->call();
return $api->json;
}
public function savezone($zone) {
$api = clone $this->http;
// We have to split up RRSets and Zoneinfo.
// First, update the zone
$zonedata = $zone;
unset($zonedata['id']);
unset($zonedata['url']);
unset($zonedata['rrsets']);
if (!isset($zone['serial']) or gettype($zone['serial']) != 'integer') {
$api->method = 'POST';
$api->url = '/zones';
$api->content = json_encode($zonedata);
$api->call();
return $api->json;
}
$api->method = 'PUT';
$api->url = $zone['url'];
$api->content = json_encode($zonedata);
$api->call();
// Then, update the rrsets
if (count($zone['rrsets']) > 0) {
$api->method = 'PATCH';
$api->content = json_encode(Array('rrsets' => $zone['rrsets']));
$api->call();
}
return $this->loadzone($zone['id']);
}
public function deletezone($zoneid) {
$api = clone $this->http;
$api->method = 'DELETE';
$api->url = "/zones/$zoneid";
$api->call();
return $api->json;
}
public function getzonekeys($zoneid) {
$ret = array();
$api = clone $this->http;
$api->method = 'GET';
$api->url = "/zones/$zoneid/cryptokeys";
$api->call();
foreach ($api->json as $key) {
if (!isset($key['active']))
continue;
$key['dstxt'] = $zoneid . ' IN DNSKEY '.$key['dnskey']."\n\n";
if (isset($key['ds'])) {
foreach ($key['ds'] as $ds) {
$key['dstxt'] .= $zoneid . ' IN DS '.$ds."\n";
}
unset($key['ds']);
}
array_push($ret, $key);
}
return $ret;
}
}
?>

View file

@ -1,376 +0,0 @@
<?php
class Zone {
public $id;
public $name;
public $kind;
public $url;
public $serial;
public $dnssec;
public $soa_edit;
public $soa_edit_api;
public $keyinfo;
public $account;
public $zone;
public $nameservers;
public $rrsets;
public $masters;
public function __construct() {
$this->id = '';
$this->name = '';
$this->kind = '';
$this->url = '';
$this->serial = '';
$this->dnssec = '';
$this->soa_edit = '';
$this->soa_edit_api = '';
$this->keyinfo = '';
$this->account = '';
$this->zone = FALSE;
$this->nameservers = Array();
$this->rrsets = Array();
$this->masters = Array();
}
public function parse($data) {
$this->setId($data['id']);
$this->setName($data['name']);
$this->setKind($data['kind']);
$this->setDnssec($data['dnssec']);
$this->setAccount($data['account']);
$this->setSerial($data['serial']);
$this->url = $data['url'];
if (isset($data['soa_edit']) && $data['soa_edit'] != "")
$this->setSoaEdit($data['soa_edit']);
if (isset($data['soa_edit_api']) && $data['soa_edit_api'] != "")
$this->setSoaEditApi($data['soa_edit_api'], True);
foreach ($data['masters'] as $master) {
$this->addMaster($master);
}
if (isset($data['rrsets'])) {
foreach ($data['rrsets'] as $rrset) {
$toadd = new RRSet($rrset['name'], $rrset['type']);
foreach ($rrset['comments'] as $comment) {
$toadd->addComment($comment['content'], $comment['account'], $comment['modified_at']);
}
foreach ($rrset['records'] as $record) {
$toadd->addRecord($record['content'], $record['disabled']);
}
$toadd->setTtl($rrset['ttl']);
array_push($this->rrsets, $toadd);
}
}
}
public function importData($data) {
$this->zone = $data;
}
public function setKeyinfo($info) {
$this->keyinfo = $info;
}
public function addNameserver($nameserver) {
foreach ($this->nameservers as $ns) {
if ($nameserver == $ns) {
throw new Exception("We already have this as a nameserver");
}
}
array_push($this->nameservers, $nameserver);
}
public function setSerial($serial) {
$this->serial = $serial;
}
public function setSoaEdit($soaedit) {
$this->soa_edit = $soaedit;
}
public function setSoaEditApi($soaeditapi, $overwrite=False) {
if (isset($this->soa_edit_api) and $this->soa_edit_api != "") {
if ($overwrite === False) {
return False;
}
}
$this->soa_edit_api = $soaeditapi;
}
public function setName($name) {
$this->name = $name;
}
public function setKind($kind) {
$this->kind = $kind;
}
public function setAccount($account) {
$this->account = $account;
}
public function setDnssec($dnssec) {
$this->dnssec = $dnssec;
}
public function setId($id) {
$this->id = $id;
}
public function addMaster($ip) {
foreach ($this->masters as $master) {
if ($ip == $master) {
throw new Exception("We already have this as a master");
}
}
array_push($this->masters, $ip);
}
public function eraseMasters() {
$this->masters = Array();
}
public function addRRSet($name, $type, $content, $disabled = FALSE, $ttl = 3600, $setptr = FALSE) {
if ($this->getRRSet($name, $type) !== FALSE) {
throw new Exception("This rrset already exists.");
}
$rrset = new RRSet($name, $type, $content, $disabled, $ttl, $setptr);
array_push($this->rrsets, $rrset);
}
public function addRecord($name, $type, $content, $disabled = FALSE, $ttl = 3600, $setptr = FALSE) {
$rrset = $this->getRRSet($name, $type);
if ($rrset) {
$rrset->addRecord($content, $disabled, $setptr);
$rrset->setTtl($ttl);
} else {
$this->addRRSet($name, $type, $content, $disabled, $ttl, $setptr);
}
return $this->getRecord($name, $type, $content);
}
public function getRecord($name, $type, $content) {
$rrset = $this->getRRSet($name, $type);
foreach ($rrset->exportRecords() as $record) {
if ($record['content'] == $content) {
$record['name'] = $rrset->name;
$record['ttl'] = $rrset->ttl;
$record['type'] = $rrset->type;
$id = json_encode($record);
$record['id'] = $id;
return $record;
}
}
}
public function getRRSet($name, $type) {
foreach ($this->rrsets as $rrset) {
if ($rrset->name == $name and $rrset->type == $type) {
return $rrset;
}
}
return FALSE;
}
public function rrsets2records() {
$ret = Array();
foreach ($this->rrsets as $rrset) {
foreach ($rrset->exportRecords() as $record) {
$record['name'] = $rrset->name;
$record['ttl'] = $rrset->ttl;
$record['type'] = $rrset->type;
$id = json_encode($record);
$record['id'] = $id;
array_push($ret, $record);
}
}
return $ret;
}
public function export() {
$ret = Array();
$ret['account'] = $this->account;
$ret['nameservers'] = $this->nameservers;
$ret['kind'] = $this->kind;
$ret['name'] = $this->name;
if (isset($this->soa_edit) && $this->soa_edit != "") {
$ret['soa_edit'] = $this->soa_edit;
}
if (isset($this->soa_edit_api) && $this->soa_edit_api != "") {
$ret['soa_edit_api'] = $this->soa_edit_api;
}
if ($this->zone) {
$ret['zone'] = $this->zone;
return $ret;
}
$ret['dnssec'] = $this->dnssec;
if ($this->dnssec) {
$ret['keyinfo'] = $this->keyinfo;
}
$ret['id'] = $this->id;
$ret['masters'] = $this->masters;
$ret['rrsets'] = $this->exportRRSets();
$ret['serial'] = $this->serial;
$ret['url'] = $this->url;
return $ret;
}
private function exportRRSets() {
$ret = Array();
foreach ($this->rrsets as $rrset) {
array_push($ret, $rrset->export());
}
return $ret;
}
}
class RRSet {
public $name;
public $type;
public $ttl;
public $changetype;
public $records;
public $comments;
public function __construct($name = '', $type = '', $content = '', $disabled = FALSE, $ttl = 3600, $setptr = FALSE) {
$this->name = $name;
$this->type = $type;
$this->ttl = $ttl;
$this->changetype = 'REPLACE';
$this->records = Array();
$this->comments = Array();
if (isset($content) and $content != '') {
$this->addRecord($content, $disabled, $setptr);
}
}
public function delete() {
$this->changetype = 'DELETE';
}
public function setTtl($ttl) {
$this->ttl = $ttl;
}
public function setName($name) {
$this->name = $name;
}
public function addRecord($content, $disabled = FALSE, $setptr = FALSE) {
foreach ($this->records as $record) {
if ($record->content == $content) {
throw new Exception($this->name."/".$this->type." has duplicate records.");
}
}
$record = new Record($content, $disabled, $setptr);
array_push($this->records, $record);
}
public function deleteRecord($content) {
foreach ($this->records as $idx => $record) {
if ($record->content == $content) {
unset($this->records[$idx]);
}
}
}
public function addComment($content, $account, $modified_at = FALSE) {
$comment = new Comment($content, $account, $modified_at);
array_push($this->comments, $comment);
}
public function export() {
$ret = Array();
$ret['comments'] = $this->exportComments();
$ret['name'] = $this->name;
$ret['records'] = $this->exportRecords();
if ($this->changetype != 'DELETE') {
$ret['ttl'] = $this->ttl;
}
$ret['type'] = $this->type;
$ret['changetype'] = $this->changetype;
return $ret;
}
public function exportRecords() {
$ret = Array();
foreach ($this->records as $record) {
if ($this->type != "A" and $this->type != "AAAA") {
$record->setptr = FALSE;
}
array_push($ret, $record->export());
}
return $ret;
}
public function exportComments() {
$ret = Array();
foreach ($this->comments as $comment) {
array_push($ret, $comment->export());
}
return $ret;
}
}
class Record {
public $content;
public $disabled;
public $setptr;
public function __construct($content, $disabled = FALSE, $setptr = FALSE) {
$this->content = $content;
$this->disabled = $disabled;
$this->setptr = $setptr;
}
public function export() {
$ret;
$ret['content'] = $this->content;
$ret['disabled'] = ( bool ) $this->disabled;
if ($this->setptr) {
$ret['set-ptr'] = ( bool ) TRUE;
}
return $ret;
}
}
class Comment {
public $content;
public $account;
public $modified_at;
public function __construct($content, $account, $modified_at) {
$this->content = $content;
$this->account = $account;
$this->modified_at = $modified_at;
}
public function export() {
$ret;
$ret['content'] = $this->content;
$ret['account'] = $this->account;
$ret['modified_at'] = $this->modified_at;
return $ret;
}
}
?>

View file

@ -1,18 +1,23 @@
<?php
$apipass = ''; # The PowerDNS API-key
$apiuser = ''; # The PowerDNS API username. Leave empty for authmethod='xapikey' (see AUTHENTICATION)
$apipass = ''; # The PowerDNS API-user password or the PowerDNS-API key (see AUTHENTICATION)
$apiip = ''; # The IP of the PowerDNS API
$apiport = '8081'; # The port of the PowerDNS API
$apivers = 0; # The version of the PowerDNS API. 0 == experimental, 1 = v1 (pdns 4.0)
$apisid = 'localhost'; # PowerDNS's :server_id
$apiproto = 'http'; # http | https
$apisslverify = FALSE; # Verify SSL Certificate if using https for apiproto
$allowzoneadd = FALSE; # Allow normal users to add zones
$logging = TRUE;
$allowclearlogs = TRUE; # Allow clearing of log entries
$allowrotatelogs = FALSE;# Allow rotation to text file on server
# Log directory - if allowrotatelogs is set, this is where the logs will
# be written. It must be writeable by the web server user.
$logsdirectory = "../etc";
### AUTHENTICATION ###
# The first versions of the PowerDNS API used the standard webserver password
# for authentication, newer versions use an X-API-Key mechanism. NSEdit tries
# to autodetect the method you should use, but that does affect performance.
# For optimal performance, configure the right method.
# (Should be 'auto', 'xapikey' or 'userpass')
$authmethod = 'auto';
# If you configure this, nsedit will try to authenticate via WeFact too.
# Debtors will be added to the sqlitedatabase with their crypted password.
@ -24,13 +29,8 @@ $logsdirectory = "../etc";
#$adminapiips = array();
#$adminapikey = 'thisshouldbequitealongstring,youknow';
# Location of user-database. Make sure its writeable and not served by the webserver!
$authdb = "../etc/pdns.users.sqlite3";
# Admin login and password at first start-up
$default_admin_username = "admin";
$default_admin_password = "admin";
# Set a random generated secret to enable auto-login and long living csrf tokens
// $secret = '...';
@ -40,27 +40,19 @@ $templates[] = array(
'name' => 'Tuxis',
'owner' => 'username', # Set to 'public' to make it available to all users
'records' => array(
array(
'name' => '',
'type' => 'MX',
'content' => '200 mx2.tuxis.nl.'),
array(
'name' => '',
'type' => 'A',
'content' => '1.2.3.4'),
array(
'name' => 'www',
'type' => 'CNAME',
'content' => '[zonename]')
array(
'name' => '',
'type' => 'MX',
'content' => '200 mx2.tuxis.nl')
)
);
*/
$defaults['soa_edit'] = 'INCEPTION-INCREMENT';
$defaults['soa_edit_api'] = 'DEFAULT';
$defaults['soa_edit_api'] = 'INCEPTION-INCREMENT';
$defaults['defaulttype'] = 'Master'; # Choose between 'Native' or 'Master'
$defaults['ns'][0] = 'unconfigured.primaryns.'; # The value of the first NS-record
$defaults['ns'][1] = 'unconfigured.secondaryns.'; # The value of the second NS-record
$defaults['ns'][0] = 'unconfigured.primaryns'; # The value of the first NS-record
$defaults['ns'][1] = 'unconfigured.secondaryns'; # The value of the second NS-record
$defaults['ttl'] = 3600; # Default TTL for records
$defaults['disabled'] = false; # Default disabled state

View file

@ -4,11 +4,21 @@ include('config.inc.php');
$blocklogin = FALSE;
if ((!isset($apipass) or empty($apipass)) or (!isset($apiip) or empty($apiip)) or (!isset($apiport) or empty($apiport))) {
if ((!isset($apipass) or empty($apipass)) or (!isset($apiip) or empty($apiip)) or (!isset($apiport) or empty($apiport)) or (!isset($apivers) or is_null($apivers))) {
$errormsg = 'You need to configure your settings for the PowerDNS API. See <a href="doc/apiconf.txt">doc/apiconf.txt</a>';
$blocklogin = TRUE;
}
if (!preg_match('/^[01]$/', $apivers)) {
$errormsg = "The value for \$apivers is incorrect your config";
$blocklogin = TRUE;
}
if (!isset($authmethod) or !preg_match('/^(xapikey|userpass|auto)$/', $authmethod)) {
$errormsg = "The value for \$authmethod is incorrect in your config";
$blocklogin = TRUE;
}
if (!isset($apiproto) or !preg_match('/^http(s)?$/', $apiproto)) {
$errormsg = "The value for \$apiproto is incorrect in your config. Did you configure it?";
$blocklogin = TRUE;
@ -35,12 +45,18 @@ if (isset($defaults['primaryns'])) {
}
if (!isset($logo) or empty($logo)) {
$logo = 'https://www.tuxis.nl/uploads/images/nsedit.png';
$logo = 'http://www.tuxis.nl/uploads/images/nsedit.png';
}
/* No need to change stuf below */
if ($apivers == 0) {
$apipath = "";
} elseif ($apivers == 1) {
$apipath = "/api/v1";
}
if (function_exists('curl_init') === FALSE) {
$errormsg = "You need PHP Curl to run nsedit";
$blocklogin = TRUE;
@ -50,7 +66,7 @@ if (class_exists('SQLite3') === FALSE) {
$errormsg = "You need PHP SQLite3 to run nsedit";
$blocklogin = TRUE;
}
if (function_exists('openssl_random_pseudo_bytes') === FALSE) {
$errormsg = "You need PHP compiled with openssl to run nsedit";
$blocklogin = TRUE;
@ -59,20 +75,13 @@ if (function_exists('openssl_random_pseudo_bytes') === FALSE) {
$defaults['defaulttype'] = ucfirst(strtolower($defaults['defaulttype']));
try {
if (isset($authdb) && !file_exists($authdb) && class_exists('SQLite3')) {
is_dir(dirname($authdb)) || mkdir(dirname($authdb));
$db = new SQLite3($authdb, SQLITE3_OPEN_CREATE|SQLITE3_OPEN_READWRITE);
$createsql = file_get_contents('includes/scheme.sql');
$db->exec($createsql);
$salt = bin2hex(openssl_random_pseudo_bytes(16));
$default_admin_username = $default_admin_username ?? "admin";
$default_admin_password = $default_admin_password ?? "admin";
$db->exec("INSERT INTO users (emailaddress, password, isadmin) VALUES ('".$default_admin_username."', '".crypt($default_admin_password, '$6$'.$salt)."', 1)");
}
} catch (Exception $e) {
print("We have issues getting the authdb working: $e");
$blocklogin = TRUE;
if (isset($authdb) && !file_exists($authdb) && class_exists('SQLite3')) {
is_dir(dirname($authdb)) || mkdir(dirname($authdb));
$db = new SQLite3($authdb, SQLITE3_OPEN_CREATE|SQLITE3_OPEN_READWRITE);
$createsql = file_get_contents('includes/scheme.sql');
$db->exec($createsql);
$salt = bin2hex(openssl_random_pseudo_bytes(16));
$db->exec("INSERT INTO users (emailaddress, password, isadmin) VALUES ('admin', '".crypt("admin", '$6$'.$salt)."', 1)");
}
function string_starts_with($string, $prefix)
@ -92,12 +101,10 @@ function string_ends_with($string, $suffix)
}
function get_db() {
global $authdb, $db;
global $authdb;
if (!isset($db)) {
$db = new SQLite3($authdb, SQLITE3_OPEN_READWRITE);
$db->exec('PRAGMA foreign_keys = 1');
}
$db = new SQLite3($authdb, SQLITE3_OPEN_READWRITE);
$db->exec('PRAGMA foreign_keys = 1');
return $db;
}
@ -119,6 +126,7 @@ function get_user_info($u) {
$q->bindValue(1, $u);
$result = $q->execute();
$userinfo = $result->fetchArray(SQLITE3_ASSOC);
$db->close();
return $userinfo;
}
@ -133,6 +141,7 @@ function do_db_auth($u, $p) {
$q->bindValue(1, $u);
$result = $q->execute();
$userinfo = $result->fetchArray(SQLITE3_ASSOC);
$db->close();
if ($userinfo and $userinfo['password'] and (crypt($p, $userinfo['password']) === $userinfo['password'])) {
return TRUE;
@ -156,16 +165,12 @@ function add_user($username, $isadmin = FALSE, $password = '') {
$q->bindValue(2, $password, SQLITE3_TEXT);
$q->bindValue(3, (int)(bool) $isadmin, SQLITE3_INTEGER);
$ret = $q->execute();
$db->close();
if ($isadmin) {
writelog("Added user $username as admin.");
} else {
writelog("Added user $username.");
}
return $ret;
}
function update_user($id, $isadmin, $password) {
function update_user($username, $isadmin, $password) {
if ($password && !preg_match('/\$6\$/', $password)) {
$salt = bin2hex(openssl_random_pseudo_bytes(16));
$password = crypt($password, '$6$'.$salt);
@ -173,49 +178,30 @@ function update_user($id, $isadmin, $password) {
$db = get_db();
$q = $db->prepare('SELECT * FROM users WHERE id = ?');
$q->bindValue(1, $id, SQLITE3_INTEGER);
$result = $q->execute();
$userinfo = $result->fetchArray(SQLITE3_ASSOC);
$q->close();
$username = $userinfo['emailaddress'];
if ($password) {
$q = $db->prepare('UPDATE users SET isadmin = ?, password = ? WHERE id = ?');
$q = $db->prepare('UPDATE users SET isadmin = ?, password = ? WHERE emailaddress = ?');
$q->bindValue(1, (int)(bool)$isadmin, SQLITE3_INTEGER);
$q->bindValue(2, $password, SQLITE3_TEXT);
$q->bindValue(3, $id, SQLITE3_INTEGER);
writelog("Updating password and/or settings for $username. Admin: ".(int)(bool)$isadmin);
$q->bindValue(3, $username, SQLITE3_TEXT);
} else {
$q = $db->prepare('UPDATE users SET isadmin = ? WHERE id = ?');
$q = $db->prepare('UPDATE users SET isadmin = ? WHERE emailaddress = ?');
$q->bindValue(1, (int)(bool)$isadmin, SQLITE3_INTEGER);
$q->bindValue(2, $id, SQLITE3_INTEGER);
writelog("Updating settings for $username. Admin: ".(int)(bool)$isadmin);
$q->bindValue(2, $username, SQLITE3_TEXT);
}
$ret = $q->execute();
$db->close();
return $ret;
}
function delete_user($id) {
$db = get_db();
$q = $db->prepare('SELECT * FROM users WHERE id = ?');
$q = $db->prepare('DELETE FROM users WHERE id = ?');
$q->bindValue(1, $id, SQLITE3_INTEGER);
$result = $q->execute();
$userinfo = $result->fetchArray(SQLITE3_ASSOC);
$q->close();
$ret = $q->execute();
$db->close();
if($userinfo) {
$q = $db->prepare('DELETE FROM users WHERE id = ?');
$q->bindValue(1, $id, SQLITE3_INTEGER);
$ret = $q->execute();
writelog("Deleted user " . $userinfo['emailaddress'] . ".");
return $ret;
} else {
return false;
}
return $ret;
}
function valid_user($name) {
@ -245,8 +231,6 @@ function jtable_respond($records, $method = 'multiple', $msg = 'Undefined errorm
$jTableResult['RecordCount'] = count($records);
}
$db = get_db();
$db->close();
header('Content-Type: application/json');
print json_encode($jTableResult);
exit(0);
@ -255,27 +239,6 @@ function jtable_respond($records, $method = 'multiple', $msg = 'Undefined errorm
function user_template_list() {
global $templates;
if (is_dir("templates.d")) {
if ($templdir=opendir("templates.d")) {
while ($entry = readdir($templdir)) {
if (!str_ends_with($entry, ".json")) {
continue;
}
$f=file_get_contents("templates.d/$entry");
if ($f === false) {
error_log("Error reading file templates.d/$entry", 0);
continue;
}
$t = json_decode($f, true);
if ($t === null) {
error_log("Error decoding templates.d/$entry", 0);
continue;
}
array_push($templates, $t);
}
}
}
$templatelist = array();
foreach ($templates as $template) {
if (is_adminuser()
@ -295,109 +258,7 @@ function user_template_names() {
return $templatenames;
}
function getlogs() {
global $logging;
if ($logging !== TRUE)
return;
$db = get_db();
$r = $db->query('SELECT * FROM logs ORDER BY timestamp DESC');
$ret = array();
while ($row = $r->fetchArray(SQLITE3_ASSOC)) {
array_push($ret, $row);
}
return $ret;
}
function clearlogs() {
global $logging;
if ($logging !== TRUE)
return;
$db = get_db();
$q = $db->query('DELETE FROM logs;');
writelog("Logtable truncated.");
}
function rotatelogs() {
global $logging, $logsdirectory;
if ($logging !== TRUE)
return FALSE;
if(!is_dir($logsdirectory) || !is_writable($logsdirectory)) {
writelog("Logs directory cannot be written to.");
return FALSE;
}
date_default_timezone_set('UTC');
$filename = date("Y-m-d-His") . ".json";
$file = fopen($logsdirectory . "/" . $filename, "x");
if($file === FALSE) {
writelog("Can't create file for log rotation.");
return FALSE;
}
if(fwrite($file,json_encode(getlogs())) === FALSE) {
writelog("Can't write to file for log rotation.");
fclose($file);
return FALSE;
} else {
fclose($file);
clearlogs();
return $filename;
}
}
function listrotatedlogs() {
global $logging, $logsdirectory;
if ($logging !== TRUE)
return FALSE;
$list = scandir($logsdirectory,SCANDIR_SORT_DESCENDING);
if($list === FALSE) {
writelog("Logs directory cannot read.");
return FALSE;
}
$list=array_filter($list,
function ($val) {
return(preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}\.json/',$val) == 1);
}
);
return $list;
}
function writelog($line, $user=False) {
global $logging;
if ($logging !== TRUE)
return;
if ($user === False) {
$user = get_sess_user();
}
try {
$db = get_db();
$q = $db->prepare('CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY,
user TEXT NOT NULL,
log TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP);');
$ret = $q->execute();
$q = $db->prepare('INSERT INTO logs (user, log) VALUES (:user, :log)');
$q->bindValue(':user', $user, SQLITE3_TEXT);
$q->bindValue(':log', $line, SQLITE3_TEXT);
$q->execute();
} catch (Exception $e) {
return jtable_respond(null, 'error', $e->getMessage());
}
}
/* This function was taken from https://gist.github.com/rsky/5104756 to make
it available on older php versions. Thanks! */

View file

@ -9,13 +9,11 @@ global $current_user;
$current_user = false;
// session startup
function _set_current_user($username, $userid, $localauth = true, $is_admin = false, $has_csrf_token = false, $is_api = false) {
function _set_current_user($username, $is_admin = false, $has_csrf_token = false, $is_api = false) {
global $current_user;
$current_user = array(
'username' => $username,
'id' => $userid,
'localauth' => $localauth,
'is_admin' => $is_admin,
'has_csrf_token' => $has_csrf_token,
'is_api' => $is_api,
@ -52,7 +50,7 @@ function _check_csrf_token($user) {
}
define('CSRF_TOKEN', $csrf_token);
header("X-CSRF-Token: {$csrf_token}");
header("X-CSRF-Token: ${csrf_token}");
}
function enc_secret($message) {
@ -122,13 +120,13 @@ function dec_secret($code) {
function _unset_cookie($name) {
$is_ssl = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off';
setcookie($name, "", -1, "", "", $is_ssl);
setcookie($name, null, -1, null, null, $is_ssl);
}
function _store_auto_login($value) {
$is_ssl = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off';
// set for 30 days
setcookie('NSEDIT_AUTOLOGIN', $value, time()+60*60*24*30, "", "", $is_ssl);
setcookie('NSEDIT_AUTOLOGIN', $value, time()+60*60*24*30, null, null, $is_ssl);
}
function try_login() {
@ -152,7 +150,6 @@ function _try_login($username, $password) {
global $wefactapiurl, $wefactapikey;
if (!valid_user($username)) {
writelog("Illegal username at login!", $username);
return false;
}
@ -161,7 +158,6 @@ function _try_login($username, $password) {
if (isset($wefactapiurl) && isset($wefactapikey)) {
$wefact = do_wefact_auth($username, $password);
if (false === $wefact ) {
writelog("Failed Wefact login!", $username);
return false;
}
if (-1 !== $wefact) {
@ -170,16 +166,14 @@ function _try_login($username, $password) {
}
if ($do_local_auth && !do_db_auth($username, $password)) {
writelog("Failed login!", $username);
return false;
}
$user = get_user_info($username);
if (!$user) {
writelog("Failed to find user!", $username);
return false;
} else {
_set_current_user($username, $user['id'], (bool) $do_local_auth, (bool) $user['isadmin']);
_set_current_user($username, (bool) $user['isadmin']);
if (session_id()) {
session_unset();
@ -189,8 +183,6 @@ function _try_login($username, $password) {
session_regenerate_id(true) or die('session failure: regenerated id failed');
session_unset();
$_SESSION['username'] = $username;
$_SESSION['localauth'] = $do_local_auth;
$_SESSION['userid'] = $user['id'];
# requires session:
_check_csrf_token($user);
@ -210,7 +202,7 @@ function _check_session() {
and $_POST['adminapikey'] === $adminapikey)
{
# Allow this request, fake that we're logged in as user.
return _set_current_user('admin', 1, false, true, true, true);
return _set_current_user('admin', true, true, true);
}
else
{
@ -226,7 +218,7 @@ function _check_session() {
session_destroy();
session_unset();
} else {
_set_current_user($_SESSION['username'], $_SESSION['userid'], (bool) $_SESSION['localauth'], (bool) $user['isadmin']);
_set_current_user($_SESSION['username'], (bool) $user['isadmin']);
_check_csrf_token($user);
return;
}
@ -285,16 +277,6 @@ function get_sess_user() {
return $current_user ? $current_user['username'] : null;
}
function get_sess_userid() {
global $current_user;
return $current_user ? $current_user['id'] : null;
}
function has_local_auth() {
global $current_user;
return $current_user ? $current_user['localauth'] : null;
}
function logout() {
@session_destroy();
@session_unset();

706
index.php
View file

@ -6,12 +6,6 @@ include_once('includes/misc.inc.php');
global $errormsg, $blocklogin;
$docroot = $_SERVER['DOCUMENT_ROOT'];
if (preg_match("@$docroot@", $authdb) == 1) {
$blocklogin = TRUE;
$errormsg = "You authdb is in your document root and probably downloadable. Please move it to a safe location!";
}
if (isset($_GET['logout']) or isset($_POST['logout'])) {
logout();
header("Location: index.php");
@ -26,7 +20,7 @@ if (!is_logged_in() and isset($_POST['formname']) and $_POST['formname'] === "lo
if (is_logged_in() and isset($_POST['formname']) and $_POST['formname'] === "changepwform") {
if (get_sess_user() == $_POST['username']) {
if (!update_user(get_sess_userid(), is_adminuser(), $_POST['password'])) {
if (!update_user(get_sess_user(), is_adminuser(), $_POST['password'])) {
$errormsg = "Unable to update password!\n";
}
} else {
@ -119,47 +113,6 @@ if ($blocklogin === TRUE) {
<div id="wrap">
<div id="dnssecinfo">
</div>
<div id="clearlogs" style="display: none;">
Are you sure you want to clear the current logs? Maybe download them
first<?php if($allowrotatelogs) { ?>, or use "Rotate logs" to save
them on the server<?php } ?>?
</div>
<div id="rotatelogs" style="display: none;">
Are you sure you want to rotate the current logs?
</div>
<div id="searchlogs" style="display: none; text-align: right;">
<table border="0">
<tr><td>User:</td><td><input type="text" id ="searchlogs-user"><br></td></tr>
<tr><td>Log Entry:</td><td><input type="text" id ="searchlogs-entry"></td></tr>
</table>
</div>
<div id="searchzone" style="display: none; text-align: right;">
<table border="0">
<tr><td>Label:</td><td><input type="text" id ="searchzone-label"><br></td></tr>
<tr><td>Type:</td><td style="text-align: left;"><select id="searchzone-type">
<option value=""></option>
<option value="A">A</option>
<option value="AAAA">AAAA</option>
<option value="CERT">CERT</option>
<option value="CNAME">CNAME</option>
<option value="ALIAS">ALIAS</option>
<option value="LOC">LOC</option>
<option value="MX">MX</option>
<option value="NAPTR">NAPTR</option>
<option value="NS">NS</option>
<option value="PTR">PTR</option>
<option value="SOA">SOA</option>
<option value="SPF">SPF</option>
<option value="SRV">SRV</option>
<option value="SSHFP">SSHFP</option>
<option value="TLSA">TLSA</option>
<option value="CAA">CAA</option>
<option value="TXT">TXT</option>
<option value="SMIMEA">SMIMEA</option>
</select><br></td></tr>
<tr><td>Content:</td><td><input type="text" id ="searchzone-content"></td></tr>
</table>
</div>
<div id="menu" class="jtable-main-container <?php if ($menutype === 'horizontal') { ?>horizontal<?php } ?>">
<div class="jtable-title menu-title">
<div class="jtable-title-text">
@ -170,7 +123,6 @@ if ($blocklogin === TRUE) {
<li><a href="#" id="zoneadmin">Zones</a></li>
<?php if (is_adminuser()) { ?>
<li><a href="#" id="useradmin">Users</a></li>
<li><a href="#" id="logadmin">Logs</a></li>
<?php } ?>
<li><a href="#" id="aboutme">About me</a></li>
<li><a href="index.php?logout=1">Logout</a></li>
@ -181,11 +133,8 @@ if ($blocklogin === TRUE) {
}
?>
<div id="zones">
<?php if ($allowzoneadd === TRUE) { ?>
<div style="display: none;" id="ImportZone"></div>
<?php } ?>
<?php if (is_adminuser()) { ?>
<div style="display: none;" id="CloneZone"></div>
<?php if (is_adminuser() or $allowzoneadd === TRUE) { ?>
<div style="visibility: hidden;" id="ImportZone"></div>
<?php } ?>
<div class="tables" id="MasterZones">
<div class="searchbar" id="searchbar">
@ -198,27 +147,8 @@ if ($blocklogin === TRUE) {
<div id="users">
<div class="tables" id="Users"></div>
</div>
<div id="logs">
<div class="tables" id="Logs"></div>
<?php if($allowrotatelogs) { ?>
<br>Log entries being viewed:
<select id="logfile">
<option value="">(Current logs)</option>
<?php
$logfiles=listrotatedlogs();
if($logfiles !== FALSE) {
foreach ($logfiles as $filename) {
echo '<option value="' . $filename . '">' . str_replace(".json","",$filename) . "</option>\n";
}
}
?></select>
<?php } else { ?>
<input type="hidden" id="logfile" value="">
<?php } ?>
</div>
<?php } ?>
<?php if (has_local_auth()) { ?>
<div id="AboutMe">
<div class="tables">
<p>Hi <?php echo get_sess_user(); ?>. You can change your password here.</p>
@ -243,11 +173,9 @@ if ($blocklogin === TRUE) {
</tr>
</table>
<input type="hidden" name="formname" value="changepwform">
<input type="hidden" name="id" value="<?php echo get_sess_userid(); ?>">
</form>
</div>
</div>
<?php } ?>
</div>
<script type="text/javascript">
window.csrf_token = '<?php echo CSRF_TOKEN ?>';
@ -296,7 +224,7 @@ function displayDnssecIcon(zone) {
function displayExportIcon(zone) {
var $img = $('<img class="list clickme" src="img/export.png" title="Export zone" />');
$img.click(function () {
var $zexport = $.getJSON("zones.php?zoneid="+zone.record.id+"&action=export", function(data) {
var $zexport = $.getJSON("zones.php?zone="+zone.record.name+"&action=export", function(data) {
blob = new Blob([data.Record.zone], { type: 'text/plain' });
var dl = document.createElement('a');
dl.addEventListener('click', function(ev) {
@ -323,11 +251,7 @@ function displayContent(fieldName, zone) {
var zspan = $('<span class="lightgrey">').text(zone);
return lspan.add(zspan);
} else {
var text = data.record[fieldName];
if (typeof data.record[fieldName] == 'boolean') {
text == false ? text = 'No' : text = 'Yes';
}
return $('<span>').text(text);
return $('<span>').text(data.record[fieldName]);
}
}
}
@ -381,16 +305,16 @@ $(document).ready(function () {
listClass: 'dnssec'
},
<?php if (is_adminuser()) { ?>
account: {
title: 'Account',
owner: {
title: 'Owner',
width: '8%',
display: displayContent('account'),
display: displayContent('owner'),
options: function(data) {
return 'users.php?action=listoptions&e='+$epoch;
},
defaultValue: 'admin',
inputClass: 'account',
listClass: 'account'
inputClass: 'owner',
listClass: 'owner'
},
<?php } ?>
kind: {
@ -439,7 +363,7 @@ $(document).ready(function () {
title: 'Records in ' + zone.record.name,
openChildAsAccordion: true,
actions: {
listAction: 'zones.php?action=listrecords&zoneid=' + zone.record.id
listAction: 'zones.php?action=listrecords&zoneurl=' + zone.record.url
},
fields: {
name: {
@ -504,227 +428,17 @@ $(document).ready(function () {
hoverAnimation: true,
hoverAnimationDuration: 60,
hoverAnimationEasing: undefined,
items: [
<?php if ($allowzoneadd === TRUE) { ?>
{
icon: 'jtable/lib/themes/metro/add.png',
text: 'Import a new zone',
click: function() {
$('#ImportZone').jtable('showCreateForm');
}
},
items: [{
<?php if (is_adminuser() or $allowzoneadd === TRUE) { ?>
icon: 'jtable/lib/themes/metro/add.png',
text: 'Import a new zone',
click: function() {
$('#ImportZone').jtable('showCreateForm');
}
<?php } ?>
<?php if (is_adminuser()) { ?>
{
icon: 'jtable/lib/themes/metro/add.png',
text: 'Clone a zone',
click: function() {
$('#CloneZone').jtable('showCreateForm');
}
},
<?php } ?>
],
}],
},
sorting: false,
selecting: true,
selectOnRowClick: true,
selectionChanged: function (data) {
var $selectedRows = $('#MasterZones').jtable('selectedRows');
$selectedRows.each(function () {
var zone = $(this).data('record');
$('#MasterZones').jtable('openChildTable',
$(this).closest('tr'), {
title: 'Records in ' + zone.name,
messages: {
addNewRecord: 'Add to ' + zone.name,
noDataAvailable: 'No records for ' + zone.name
},
toolbar: {
items: [
{
text: 'Search zone',
click: function() {
$("#searchzone").dialog({
modal: true,
title: "Search zone for ...",
width: 'auto',
buttons: {
Search: function() {
$( this ).dialog( 'close' );
opentable.find('.jtable-title-text').text(opentableTitle + " (filtered)");
opentable.jtable('load', {
label: $('#searchzone-label').val(),
type: $('#searchzone-type').val(),
content: $('#searchzone-content').val()
});
},
Reset: function() {
$('#searchzone-label').val('');
$('#searchzone-type').val('');
$('#searchzone-content').val('');
$( this ).dialog( 'close' );
opentable.find('.jtable-title-text').text(opentableTitle);
opentable.jtable('load');
return false;
}
}
});
}
}
],
},
paging: true,
sorting: true,
pageSize: 20,
openChildAsAccordion: true,
actions: {
listAction: 'zones.php?action=listrecords&zoneid=' + zone.id,
createAction: 'zones.php?action=createrecord&zoneid=' + zone.id,
deleteAction: 'zones.php?action=deleterecord&zoneid=' + zone.id,
updateAction: 'zones.php?action=editrecord&zoneid=' + zone.id
},
fields: {
domid: {
create: true,
type: 'hidden',
defaultValue: zone.id
},
id: {
key: true,
type: 'hidden',
create: false,
edit: false,
list: false
},
domain: {
create: true,
type: 'hidden',
defaultValue: zone.name
},
name: {
title: 'Label',
width: '7%',
sorting: true,
create: true,
display: displayContent('name', zone.name),
inputClass: 'name',
listClass: 'name'
},
type: {
title: 'Type',
width: '2%',
options: function() {
zonename = new String(zone.name);
if (zonename.match(/(\.in-addr|\.ip6)\.arpa/)) {
return {
'PTR': 'PTR',
'NS': 'NS',
'MX': 'MX',
'TXT': 'TXT',
'SOA': 'SOA',
'A': 'A',
'AAAA': 'AAAA',
'CERT': 'CERT',
'CNAME': 'CNAME',
'ALIAS': 'ALIAS',
'LOC': 'LOC',
'NAPTR': 'NAPTR',
'SPF': 'SPF',
'SRV': 'SRV',
'SSHFP': 'SSHFP',
'TLSA': 'TLSA',
'CAA': 'CAA',
'DNAME': 'DNAME',
'DS': 'DS',
'SMIMEA': 'SMIMEA'
};
}
return {
'A': 'A',
'AAAA': 'AAAA',
'CERT': 'CERT',
'CNAME': 'CNAME',
'DNAME': 'DNAME',
'ALIAS': 'ALIAS',
'DS': 'DS',
'LOC': 'LOC',
'MX': 'MX',
'NAPTR': 'NAPTR',
'NS': 'NS',
'PTR': 'PTR',
'SOA': 'SOA',
'SPF': 'SPF',
'SRV': 'SRV',
'SSHFP': 'SSHFP',
'TLSA': 'TLSA',
'CAA': 'CAA',
'TXT': 'TXT',
'SMIMEA': 'SMIMEA'
};
},
display: displayContent('type'),
create: true,
inputClass: 'type',
listClass: 'type'
},
content: {
title: 'Content',
width: '30%',
create: true,
sorting: true,
display: displayContent('content'),
inputClass: 'content',
listClass: 'content'
},
ttl: {
title: 'TTL',
width: '2%',
create: true,
sorting: false,
display: displayContent('ttl'),
defaultValue: '<?php echo $defaults['ttl']; ?>',
inputClass: 'ttl',
listClass: 'ttl'
},
setptr: {
title: 'Set PTR Record',
width: '2%',
list: false,
create: true,
defaultValue: 'false',
inputClass: 'setptr',
listClass: 'setptr',
options: function() {
return {
'0': 'No',
'1': 'Yes',
};
},
},
disabled: {
title: 'Disabled',
width: '2%',
create: true,
sorting: false,
display: displayContent('disabled'),
defaultValue: '<?php echo $defaults['disabled'] ? 'No' : 'Yes'; ?>',
inputClass: 'disabled',
listClass: 'disabled',
options: function() {
return {
'0': 'No',
'1': 'Yes',
};
},
},
}
}, function (data) {
opentable=data.childTable;
opentableTitle=opentable.find('.jtable-title-text').text();
data.childTable.jtable('load');
});
});
},
openChildAsAccordion: true,
actions: {
listAction: 'zones.php?action=list',
@ -758,16 +472,16 @@ $(document).ready(function () {
listClass: 'dnssec'
},
<?php if (is_adminuser()) { ?>
account: {
title: 'Account',
owner: {
title: 'Owner',
width: '8%',
display: displayContent('account'),
display: displayContent('owner'),
options: function(data) {
return 'users.php?action=listoptions&e='+$epoch;
},
defaultValue: 'admin',
inputClass: 'account',
listClass: 'account'
inputClass: 'owner',
listClass: 'owner'
},
<?php } ?>
kind: {
@ -819,6 +533,143 @@ $(document).ready(function () {
inputClass: 'serial',
listClass: 'serial'
},
records: {
width: '5%',
title: 'Records',
edit: false,
create: false,
display: function (zone) {
var $img = $('<img class="list" src="img/list.png" title="Records" />');
$img.click(function () {
$('#MasterZones').jtable('openChildTable',
$img.closest('tr'), {
title: 'Records in ' + zone.record.name,
messages: {
addNewRecord: 'Add to ' + zone.record.name,
noDataAvailable: 'No records for ' + zone.record.name
},
paging: true,
pageSize: 20,
openChildAsAccordion: true,
actions: {
listAction: 'zones.php?action=listrecords&zoneurl=' + zone.record.url,
createAction: 'zones.php?action=createrecord&zoneurl=' + zone.record.url,
deleteAction: 'zones.php?action=deleterecord&zoneurl=' + zone.record.url,
updateAction: 'zones.php?action=editrecord&zoneurl=' + zone.record.url
},
fields: {
domid: {
create: true,
type: 'hidden',
defaultValue: zone.record.id
},
id: {
key: true,
type: 'hidden',
create: false,
edit: false,
list: false
},
domain: {
create: true,
type: 'hidden',
defaultValue: zone.record.name
},
name: {
title: 'Label',
width: '7%',
create: true,
display: displayContent('name', zone.record.name),
inputClass: 'name',
listClass: 'name'
},
type: {
title: 'Type',
width: '2%',
options: function() {
zonename = new String(zone.record.name);
if (zonename.match(/(\.in-addr|\.ip6)\.arpa/)) {
return {
'PTR': 'PTR',
'NS': 'NS',
'MX': 'MX',
'TXT': 'TXT',
'SOA': 'SOA',
'A': 'A',
'AAAA': 'AAAA',
'CERT': 'CERT',
'CNAME': 'CNAME',
'LOC': 'LOC',
'NAPTR': 'NAPTR',
'SPF': 'SPF',
'SRV': 'SRV',
'SSHFP': 'SSHFP',
'TLSA': 'TLSA',
};
}
return {
'A': 'A',
'AAAA': 'AAAA',
'CERT': 'CERT',
'CNAME': 'CNAME',
'LOC': 'LOC',
'MX': 'MX',
'NAPTR': 'NAPTR',
'NS': 'NS',
'PTR': 'PTR',
'SOA': 'SOA',
'SPF': 'SPF',
'SRV': 'SRV',
'SSHFP': 'SSHFP',
'TLSA': 'TLSA',
'TXT': 'TXT',
};
},
display: displayContent('type'),
create: true,
inputClass: 'type',
listClass: 'type'
},
content: {
title: 'Content',
width: '30%',
create: true,
display: displayContent('content'),
inputClass: 'content',
listClass: 'content'
},
ttl: {
title: 'TTL',
width: '2%',
create: true,
display: displayContent('ttl'),
defaultValue: '<?php echo $defaults['ttl']; ?>',
inputClass: 'ttl',
listClass: 'ttl'
},
disabled: {
title: 'Disabled',
width: '2%',
create: true,
display: displayContent('disabled'),
defaultValue: '<?php echo $defaults['disabled'] ? 'false' : 'true'; ?>',
inputClass: 'disabled',
listClass: 'disabled',
options: function() {
return {
'0': 'false',
'1': 'true',
};
},
},
}
}, function (data) {
data.childTable.jtable('load');
})
});
return $img;
}
},
exportzone: {
title: '',
width: '1%',
@ -844,13 +695,13 @@ $(document).ready(function () {
inputClass: 'domain'
},
<?php if (is_adminuser()) { ?>
account: {
title: 'Account',
owner: {
title: 'Owner',
options: function(data) {
return 'users.php?action=listoptions&e='+$epoch;
},
defaultValue: 'admin',
inputClass: 'account'
inputClass: 'owner'
},
<?php } ?>
kind: {
@ -870,7 +721,7 @@ $(document).ready(function () {
type: 'checkbox',
values: {'0': 'No', '1': 'Yes'},
defaultValue: 1,
inputClass: 'overwrite_nameserver'
inputClass: 'overwrite_namerserver'
},
nameserver: {
title: 'Nameservers',
@ -891,50 +742,6 @@ $(document).ready(function () {
}
});
$('#CloneZone').jtable({
title: 'Clone zone',
actions: {
createAction: 'zones.php?action=clone'
},
fields: {
id: {
key: true,
type: 'hidden'
},
sourcename: {
title: 'Source domain',
options: function(data) {
return 'zones.php?action=formzonelist&e='+$epoch;
},
inputClass: 'sourcename'
},
destname: {
title: 'Domain',
inputClass: 'destname'
},
account: {
title: 'Account',
options: function(data) {
return 'users.php?action=listoptions&e='+$epoch;
},
defaultValue: 'admin',
inputClass: 'account'
},
kind: {
title: 'Type',
options: {'Native': 'Native', 'Master': 'Master'},
defaultValue: '<?php echo $defaults['defaulttype']; ?>',
edit: false,
inputClass: 'type'
},
},
recordAdded: function() {
$("#MasterZones").jtable('load');
$("#SlaveZones").jtable('load');
}
});
$('#domsearch').addClear({
onClear: function() { $('#MasterZones').jtable('load'); }
});
@ -965,41 +772,26 @@ $(document).ready(function () {
});
<?php if (is_adminuser()) { ?>
$('#logs').hide();
$('#Users').hide();
$('#AboutMe').hide();
$('#aboutme').click(function () {
$('#logs').hide();
$('#Users').hide();
$('#MasterZones').hide();
$('#SlaveZones').hide();
$('#AboutMe').show();
});
$('#useradmin').click(function () {
$('#logs').hide();
$('#Users').show();
$('#MasterZones').hide();
$('#SlaveZones').hide();
$('#AboutMe').hide();
$('#Users').jtable('load');
$('#Users').show();
});
$('#zoneadmin').click(function () {
$('#logs').hide();
$('#Users').hide();
$('#AboutMe').hide();
$('#MasterZones').show();
$('#SlaveZones').show();
});
$('#logadmin').click(function () {
$('#Users').hide();
$('#AboutMe').hide();
$('#MasterZones').hide();
$('#SlaveZones').hide();
$('#Logs').jtable('load', {
logfile: $('#logfile').val()
});
$('#logs').show();
});
$('#Users').jtable({
title: 'Users',
paging: true,
@ -1024,7 +816,6 @@ $(document).ready(function () {
title: 'User',
display: displayContent('emailaddress'),
inputClass: 'emailaddress',
edit: false,
listClass: 'emailaddress'
},
password: {
@ -1047,170 +838,7 @@ $(document).ready(function () {
$("#SlaveZones").jtable('reload');
}
});
$('#Logs').jtable({
title: 'Logs',
paging: true,
pageSize: 20,
sorting: false,
actions: {
listAction: 'logs.php?action=list'
},
messages: {
deleteConfirmation: 'This entry will be deleted. Are you sure?'
},
toolbar: {
hoverAnimation: true,
hoverAnimationDuration: 60,
hoverAnimationEasing: undefined,
items: [
{
text: 'Search logs',
click: function() {
$("#searchlogs").dialog({
modal: true,
title: "Search logs for ...",
width: 'auto',
buttons: {
Search: function() {
$( this ).dialog( 'close' );
$('#Logs').find('.jtable-title-text').text('Logs (filtered)');
$('#Logs').jtable('load', {
logfile: $('#logfile').val(),
user: $('#searchlogs-user').val(),
entry: $('#searchlogs-entry').val()
});
},
Reset: function() {
$('#searchlogs-user').val('');
$('#searchlogs-entry').val('');
$( this ).dialog( 'close' );
$('#Logs').find('.jtable-title-text').text('Logs');
$('#Logs').jtable('load', {
logfile: $('#logfile').val()
});
return false;
}
}
});
}
},
<?php if($allowrotatelogs === TRUE) { ?>
{
text: 'Rotate logs',
click: function() {
$("#rotatelogs").dialog({
modal: true,
title: "Rotate logs",
width: 'auto',
buttons: {
Ok: function() {
$.get("logs.php?action=rotate");
$( this ).dialog( "close" );
$('#logfile').val('');
$('#Logs').jtable('load');
},
Cancel: function() {
$( this ).dialog( "close" );
return false;
}
}
});
}
},
<?php } ?>
<?php if($allowclearlogs === TRUE) { ?>
{
icon: 'img/delete_inverted.png',
text: 'Clear logs',
click: function() {
$("#clearlogs").dialog({
modal: true,
title: "Clear all logs",
width: 'auto',
buttons: {
Ok: function() {
$.get("logs.php?action=clear");
$( this ).dialog( "close" );
$('#logfile').val('');
$('#Logs').jtable('load');
},
Cancel: function() {
$( this ).dialog( "close" );
return false;
}
}
});
}
},
<?php } ?>
{
icon: 'img/export.png',
text: 'Download logs',
click: function () {
var $zexport = $.get("logs.php?action=export&logfile=" + $('#logfile').val(), function(data) {
console.log(data);
blob = new Blob([data], { type: 'text/plain' });
var dl = document.createElement('a');
dl.addEventListener('click', function(ev) {
dl.href = URL.createObjectURL(blob);
dl.download = $('#logfile').val() == "" ? 'nseditlogs.txt':$('#logfile').val() + ".txt";
}, false);
if (document.createEvent) {
var event = document.createEvent("MouseEvents");
event.initEvent("click", true, true);
dl.dispatchEvent(event);
}
});
}
}
],
},
fields: {
id: {
title: 'key',
key: true,
type: 'hidden'
},
user: {
title: 'User',
width: '10%',
display: displayContent('user'),
},
log: {
title: 'Log',
width: '80%',
display: displayContent('log'),
},
timestamp: {
title: 'Timestamp',
width: '10%',
display: displayContent('timestamp')
}
}
});
$('#logfile').change(function () {
$('#Logs').jtable('load', {
logfile: $('#logfile').val(),
user: $('#searchlogs-user').val(),
entry: $('#searchlogs-entry').val()
});
});
<?php } else { ?>
$('#AboutMe').hide();
$('#aboutme').click(function () {
$('#MasterZones').hide();
$('#SlaveZones').hide();
$('#AboutMe').show();
});
$('#zoneadmin').click(function () {
$('#AboutMe').hide();
$('#MasterZones').show();
$('#SlaveZones').show();
});
$('#Users').jtable('load');
<?php } ?>
$('#MasterZones').jtable('load');
$('#SlaveZones').jtable('load');

View file

@ -1,96 +0,0 @@
<?php
include_once('includes/config.inc.php');
include_once('includes/session.inc.php');
include_once('includes/misc.inc.php');
if (!is_csrf_safe()) {
header('Status: 403');
header('Location: ./index.php');
jtable_respond(null, 'error', "Authentication required");
}
if (!is_adminuser()) {
header('Status: 403');
jtable_respond(null, 'error', "You need adminprivileges to get here");
}
if (!isset($_GET['action'])) {
header('Status: 400');
jtable_respond(null, 'error', 'No action given');
}
if ($logging !== TRUE) {
jtable_respond(null, 'error', 'Logging is disabled');
} else {
switch ($_GET['action']) {
case "list":
if(!empty($_POST['logfile'])) {
if(preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}\.json/',$_POST['logfile']) == 1) {
$entries=json_decode(file_get_contents($logsdirectory . "/" . $_POST['logfile']),true);
} else {
jtable_respond(null, 'error', "Can't find log file");
break;
}
} else {
$entries=getlogs();
}
if(!empty($_POST['user'])) {
$entries=array_filter($entries,
function ($val) {
return(stripos($val['user'], $_POST['user']) !== FALSE);
}
);
}
if(!empty($_POST['entry'])) {
$entries=array_filter($entries,
function ($val) {
return(stripos($val['log'], $_POST['entry']) !== FALSE);
}
);
}
jtable_respond($entries);
break;
case "export":
if(!empty($_GET['logfile'])) {
if(preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}\.json/',$_GET['logfile']) == 1) {
$entries=json_decode(file_get_contents($logsdirectory . "/" . $_GET['logfile']),true);
} else {
jtable_respond(null, 'error', "Can't find log file");
break;
}
} else {
$entries=getlogs();
}
if(defined('JSON_PRETTY_PRINT')) {
print json_encode($entries,JSON_PRETTY_PRINT);
} else {
print json_encode($entries);
}
break;
case "clear":
if($allowclearlogs === TRUE) {
clearlogs();
} else {
jtable_respond(null, 'error', 'Invalid action');
}
break;
case "rotate":
if($allowrotatelogs === TRUE) {
rotatelogs();
} else {
jtable_respond(null, 'error', 'Invalid action');
}
break;
default:
jtable_respond(null, 'error', 'Invalid action');
break;
}
}

View file

@ -1,16 +0,0 @@
<?php
include_once('includes/config.inc.php');
include_once('includes/session.inc.php');
include_once('includes/misc.inc.php');
if(php_sapi_name() !== 'cli') {
echo "This script is intended to be run from the command line";
} else {
if($allowrotatelogs === TRUE) {
$current_user['username']='<system>';
rotatelogs();
} else {
echo "Rotating logs has been disabled."
}
}

View file

@ -1,28 +0,0 @@
{
"name": "Example Template",
"owner": "public",
"records": [
{
"name": "",
"type": "NS",
"content": "ns1.example.com.",
"label": "ns1"
},
{
"name": "",
"type": "NS",
"content": "ns2.example.com.",
"label": "ns2"
},
{
"name": "example-txt",
"type": "TXT",
"content": "This is an example txt record"
},
{
"name": "localhost",
"type": "A",
"content": "127.0.0.1"
}
]
}

View file

@ -64,13 +64,20 @@ case "create":
break;
case "update":
$id = isset($_POST['id']) ? intval($_POST['id']) : '';
$emailaddress = isset($_POST['emailaddress']) ? $_POST['emailaddress'] : '';
$isadmin = isset($_POST['isadmin']) ? $_POST['isadmin'] : '0';
$password = isset($_POST['password']) ? $_POST['password'] : '';
if ($id != '' and update_user($id, $isadmin, $password)) {
$result = array('isadmin' => $isadmin);
if (!valid_user($emailaddress)) {
jtable_respond(null, 'error', "Please only use ^[a-z0-9@_.-]+$ for usernames");
}
if (!user_exists($emailaddress)) {
jtable_respond(null, 'error', 'Cannot update not existing user');
}
if (update_user($emailaddress, $isadmin, $password)) {
$result = array('emailaddress' => $emailaddress, 'isadmin' => $isadmin);
jtable_respond($result, 'single');
} else {
jtable_respond(null, 'error', 'Could not update user');
@ -78,9 +85,7 @@ case "update":
break;
case "delete":
$id = isset($_POST['id']) ? intval($_POST['id']) : '';
if ($id != '' and delete_user($id) !== FALSE) {
if (delete_user($_POST['id']) !== FALSE) {
jtable_respond(null, 'delete');
} else {
jtable_respond(null, 'error', 'Could not delete user');

711
zones.php
View file

@ -3,8 +3,6 @@
include_once('includes/config.inc.php');
include_once('includes/session.inc.php');
include_once('includes/misc.inc.php');
include_once('includes/class/PdnsApi.php');
include_once('includes/class/Zone.php');
if (!is_csrf_safe()) {
header('Status: 403');
@ -12,18 +10,236 @@ if (!is_csrf_safe()) {
jtable_respond(null, 'error', "Authentication required");
}
function api_request($path, $opts = null, $type = null) {
global $apiproto, $apisslverify, $apisid, $apiuser, $apipass, $apiip, $apiport, $authmethod, $apipath;
$quoteus = array('TXT', 'SPF');
$url = "$apiproto://$apiip:$apiport${apipath}${path}";
if ($authmethod == "auto") {
$ad = curl_init();
if ( strcasecmp( $apiproto, 'https' ) == 0 ) {
curl_setopt($ad, CURLOPT_SSL_VERIFYPEER, $apisslverify);
}
curl_setopt($ad, CURLOPT_HTTPHEADER, array('X-API-Key: '.$apipass));
curl_setopt($ad, CURLOPT_URL, "$apiproto://$apiip:$apiport/servers/localhost/statistics");
curl_setopt($ad, CURLOPT_RETURNTRANSFER, 1);
curl_exec($ad);
if (curl_getinfo($ad, CURLINFO_HTTP_CODE) == 401) {
$authmethod = 'userpass';
} else {
$authmethod = 'xapikey';
}
}
$headers = array();
array_push($headers, 'Accept: application/json');
$ch = curl_init();
if ($authmethod == "xapikey") {
array_push($headers, 'X-API-Key: '.$apipass);
} else {
curl_setopt($ch, CURLOPT_USERPWD, "$apiuser:$apipass");
}
if ( strcasecmp( $apiproto, 'https' ) == 0 ) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $apisslverify);
}
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
if ($opts) {
if (!$type) {
$type = 'POST';
}
$postdata = json_encode($opts);
array_push($headers, 'Content-Type: application/json');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postdata);
}
switch ($type) {
case 'DELETE':
case 'PATCH':
case 'PUT':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $type);
break;
case 'POST':
break;
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$return = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$json = json_decode($return, 1);
if (isset($json['error'])) {
jtable_respond(null, 'error', "API Error $code: ".$json['error']);
} elseif ($code < 200 || $code >= 300) {
if ($code == 401) {
$code = "Authentication failed. Have you configured your authmethod correct?";
}
jtable_respond(null, 'error', "API Error: $code");
}
return $json;
}
function zones_api_request($opts = null, $type = 'POST') {
global $apisid;
return api_request("/servers/${apisid}/zones", $opts, $type);
}
function get_all_zones() {
return zones_api_request();
}
function _get_zone_by_key($key, $value) {
if ($value !== '') {
foreach (get_all_zones() as $zone) {
if ($zone[$key] === $value) {
$zone['owner'] = get_zone_owner($zone['name'], 'admin');
if (!check_owner($zone)) {
jtable_respond(null, 'error', 'Access denied');
}
return $zone;
}
}
}
header('Status: 404 Not found');
jtable_respond(null, 'error', "Zone not found");
}
function get_zone_by_url($zoneurl) {
return _get_zone_by_key('url', $zoneurl);
}
function get_zone_by_id($zoneid) {
return _get_zone_by_key('id', $zoneid);
}
function get_zone_by_name($zonename) {
return _get_zone_by_key('name', $zonename);
}
/* This function is taken from:
http://pageconfig.com/post/how-to-validate-ascii-text-in-php and got fixed by
#powerdns */
function is_ascii($string) {
return ( bool ) ! preg_match( '/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x80-\\xff]/' , $string );
}
function _valid_label($name) {
return is_ascii($name) && ( bool ) preg_match("/^([-.a-z0-9_\/\*]+)?.$/i", $name );
return is_ascii($name) && ( bool ) preg_match("/^([-.a-z0-9_\/\*]+)?$/i", $name );
}
function make_record($zone, $input) {
global $defaults;
$name = isset($input['name']) ? $input['name'] : '';
if ('' == $name) {
$name = $zone['name'];
} elseif (string_ends_with($name, '.')) {
# "absolute" name, shouldn't append zone[name] - but check.
$name = substr($name, 0, -1);
if (!string_ends_with($name, $zone['name'])) {
jtable_respond(null, 'error', "Name $name not in zone ".$zone['name']);
}
} else if (!string_ends_with($name, $zone['name'])) {
$name = $name . '.' . $zone['name'];
}
$ttl = (int) ((isset($input['ttl']) && $input['ttl']) ? $input['ttl'] : $defaults['ttl']);
$type = isset($input['type']) ? $input['type'] : '';
$disabled = (bool) (isset($input['disabled']) && $input['disabled']);
$content = isset($input['content']) ? $input['content'] : '';
if ($type === 'TXT') {
# empty TXT records are ok, otherwise require surrounding quotes: "..."
if (strlen($content) == 1 || substr($content, 0, 1) !== '"' || substr($content, -1) !== '"') {
# fix quoting: first escape all \, then all ", then surround with quotes.
$content = '"'.str_replace('"', '\\"', str_replace('\\', '\\\\', $content)).'"';
}
}
if (!_valid_label($name)) {
jtable_respond(null, 'error', "Please only use [a-z0-9_/.-]");
}
if (!$type) {
jtable_respond(null, 'error', "Require a type");
}
if (!is_ascii($content)) {
jtable_respond(null, 'error', "Please only use ASCII-characters in your fields");
}
return array(
'disabled' => $disabled,
'type' => $type,
'name' => $name,
'ttl' => $ttl,
'content' => $content);
}
function update_records($zone, $name_and_type, $inputs) {
# need one "record" to extract name and type, in case we have no inputs
# (deletion of all records)
$name_and_type = make_record($zone, $name_and_type);
$name = $name_and_type['name'];
$type = $name_and_type['type'];
$records = array();
foreach ($inputs as $input) {
$record = make_record($zone, $input);
if ($record['name'] !== $name || $record['type'] !== $type) {
jtable_respond(null, 'error', "Records not matching");
}
array_push($records, $record);
}
if (!_valid_label($name)) {
jtable_respond(null, 'error', "Please only use [a-z0-9_/.-]");
}
$patch = array(
'rrsets' => array(array(
'name' => $name,
'type' => $type,
'changetype' => count($records) ? 'REPLACE' : 'DELETE',
'records' => $records)));
api_request($zone['url'], $patch, 'PATCH');
}
function create_record($zone, $input) {
$record = make_record($zone, $input);
$records = get_records_by_name_type($zone, $record['name'], $record['type']);
array_push($records, $record);
$patch = array(
'rrsets' => array(array(
'name' => $record['name'],
'type' => $record['type'],
'changetype' => 'REPLACE',
'records' => $records)));
api_request($zone['url'], $patch, 'PATCH');
return $record;
}
function get_records_by_name_type($zone, $name, $type) {
$zone = api_request($zone['url']);
$records = array();
foreach ($zone['records'] as $record) {
if ($record['name'] == $name and $record['type'] == $type) {
array_push($records, $record);
}
}
return $records;
}
function decode_record_id($id) {
@ -39,6 +255,39 @@ function decode_record_id($id) {
return $record;
}
# get all records with same name and type but different id (content)
# requires records with id to be present
# SOA records match always, regardless of content.
function get_records_except($zone, $exclude) {
$is_soa = ($exclude['type'] == 'SOA');
$found = false;
$zone = api_request($zone['url']);
$records = array();
foreach ($zone['records'] as $record) {
if ($record['name'] == $exclude['name'] and $record['type'] == $exclude['type']) {
if ($is_soa) {
# SOA changes all the time (serial); we can't match it in a sane way.
# OTOH we know it is unique anyway - just pretend we found a match.
$found = true;
} elseif ($record['content'] != $exclude['content']
or $record['ttl'] != $exclude['ttl']
or $record['disabled'] != $exclude['disabled']) {
array_push($records, $record);
} else {
$found = true;
}
}
}
if (!$found) {
header("Status: 404 Not Found");
jtable_respond(null, 'error', "Didn't find record with id");
}
return $records;
}
function compareName($a, $b) {
$a = array_reverse(explode('.', $a));
$b = array_reverse(explode('.', $b));
@ -72,48 +321,31 @@ function rrtype_compare($a, $b) {
}
}
function record_compare_default($a, $b) {
function record_compare($a, $b) {
if ($cmp = compareName($a['name'], $b['name'])) return $cmp;
if ($cmp = rrtype_compare($a['type'], $b['type'])) return $cmp;
if ($cmp = strnatcasecmp($a['content'], $b['content'])) return $cmp;
return 0;
}
function record_compare_name($a, $b) {
return record_compare_default($a, $b);
}
function record_compare_type($a, $b) {
if ($cmp = rrtype_compare($a['type'], $b['type'])) return $cmp;
if ($cmp = compareName($a['name'], $b['name'])) return $cmp;
if ($cmp = strnatcasecmp($a['content'], $b['content'])) return $cmp;
return 0;
}
function record_compare_content($a, $b) {
if ($cmp = strnatcasecmp($a['content'], $b['content'])) return $cmp;
if ($cmp = compareName($a['name'], $b['name'])) return $cmp;
if ($cmp = rrtype_compare($a['type'], $b['type'])) return $cmp;
return 0;
}
function add_db_zone($zonename, $accountname) {
if (valid_user($accountname) === false) {
jtable_respond(null, 'error', "$accountname is not a valid username");
function add_db_zone($zonename, $ownername) {
if (valid_user($ownername) === false) {
jtable_respond(null, 'error', "$ownername is not a valid username");
}
if (!_valid_label($zonename)) {
jtable_respond(null, 'error', "$zonename is not a valid zonename");
}
if (is_apiuser() && !user_exists($accountname)) {
add_user($accountname);
if (is_apiuser() && !user_exists($ownername)) {
add_user($ownername);
}
$db = get_db();
$q = $db->prepare("INSERT OR REPLACE INTO zones (zone, owner) VALUES (?, (SELECT id FROM users WHERE emailaddress = ?))");
$q->bindValue(1, $zonename, SQLITE3_TEXT);
$q->bindValue(2, $accountname, SQLITE3_TEXT);
$q->bindValue(2, $ownername, SQLITE3_TEXT);
$q->execute();
$db->close();
}
function delete_db_zone($zonename) {
@ -124,9 +356,10 @@ function delete_db_zone($zonename) {
$q = $db->prepare("DELETE FROM zones WHERE zone = ?");
$q->bindValue(1, $zonename, SQLITE3_TEXT);
$q->execute();
$db->close();
}
function get_zone_account($zonename, $default) {
function get_zone_owner($zonename, $default) {
if (!_valid_label($zonename)) {
jtable_respond(null, 'error', "$zonename is not a valid zonename");
}
@ -135,6 +368,7 @@ function get_zone_account($zonename, $default) {
$q->bindValue(1, $zonename, SQLITE3_TEXT);
$result = $q->execute();
$zoneinfo = $result->fetchArray(SQLITE3_ASSOC);
$db->close();
if (isset($zoneinfo['emailaddress']) && $zoneinfo['emailaddress'] != null ) {
return $zoneinfo['emailaddress'];
}
@ -142,18 +376,28 @@ function get_zone_account($zonename, $default) {
return $default;
}
function quote_content($content) {
# empty TXT records are ok, otherwise require surrounding quotes: "..."
if (strlen($content) == 1 || substr($content, 0, 1) !== '"' || substr($content, -1) !== '"') {
# fix quoting: first escape all \, then all ", then surround with quotes.
$content = '"'.str_replace('"', '\\"', str_replace('\\', '\\\\', $content)).'"';
function get_zone_keys($zone) {
$ret = array();
foreach (api_request($zone['url'] . "/cryptokeys") as $key) {
if (!isset($key['active']))
continue;
$key['dstxt'] = $zone['name'] . ' IN DNSKEY '.$key['dnskey']."\n\n";
if (isset($key['ds'])) {
foreach ($key['ds'] as $ds) {
$key['dstxt'] .= $zone['name'] . ' IN DS '.$ds."\n";
}
unset($key['ds']);
}
$ret[] = $key;
}
return $content;
return $ret;
}
function check_account($zone) {
return is_adminuser() or ($zone->account === get_sess_user());
function check_owner($zone) {
return is_adminuser() or ($zone['owner'] === get_sess_user());
}
if (isset($_GET['action'])) {
@ -162,100 +406,34 @@ if (isset($_GET['action'])) {
jtable_respond(null, 'error', 'No action given');
}
try {
$api = new PdnsAPI;
switch ($action) {
case "list":
case "listslaves":
$return = Array();
$return = array();
$q = isset($_POST['domsearch']) ? $_POST['domsearch'] : false;
foreach ($api->listzones($q) as $sresult) {
$zone = new Zone();
$zone->parse($sresult);
if ($zone->account == '') {
$zone->setAccount(get_zone_account($zone->name, 'admin'));
}
if (!check_account($zone))
foreach (get_all_zones() as $zone) {
$zone['owner'] = get_zone_owner($zone['name'], 'admin');
if (!check_owner($zone))
continue;
if ($action == "listslaves" and $zone->kind == "Slave") {
array_push($return, $zone->export());
} elseif ($action == "list" and $zone->kind != "Slave") {
if ($zone->dnssec) {
$zone->setKeyinfo($api->getzonekeys($zone->id));
if ($q && !preg_match("/$q/", $zone['name'])) {
continue;
}
if ($action == "listslaves" and $zone['kind'] == "Slave") {
array_push($return, $zone);
} elseif ($action == "list" and $zone['kind'] != "Slave") {
if ($zone['dnssec']) {
$zone['keyinfo'] = get_zone_keys($zone);
}
array_push($return, $zone->export());
array_push($return, $zone);
}
}
usort($return, "zone_compare");
jtable_respond($return);
break;
case "listrecords":
$zonedata = $api->loadzone($_GET['zoneid']);
$zone = new Zone();
$zone->parse($zonedata);
$records = $zone->rrsets2records();
if(!empty($_POST['label'])) {
$records=array_filter($records,
function ($val) {
return(stripos($val['name'], $_POST['label']) !== FALSE);
}
);
}
if(!empty($_POST['type'])) {
$records=array_filter($records,
function ($val) {
return($val['type'] == $_POST['type']);
}
);
}
if(!empty($_POST['content'])) {
$records=array_filter($records,
function ($val) {
return(stripos($val['content'], $_POST['content']) !== FALSE);
}
);
}
if (isset($_GET['jtSorting'])) {
list($scolumn, $sorder) = preg_split("/ /", $_GET['jtSorting']);
switch ($scolumn) {
case "type":
usort($records, "record_compare_type");
break;
case "content":
usort($records, "record_compare_content");
break;
default:
usort($records, "record_compare_name");
break;
}
if ($sorder == "DESC") {
$records = array_reverse($records);
}
} else {
usort($records, "record_compare_name");
}
jtable_respond($records);
break;
case "delete":
$zone = $api->loadzone($_POST['id']);
$api->deletezone($_POST['id']);
delete_db_zone($zone['name']);
writelog("Deleted zone ".$zone['name']);
jtable_respond(null, 'delete');
break;
case "create":
$zonename = isset($_POST['name']) ? $_POST['name'] : '';
$zonekind = isset($_POST['kind']) ? $_POST['kind'] : '';
@ -271,50 +449,55 @@ case "create":
jtable_respond(null, 'error', "Not enough data");
}
$zone = new Zone();
$zone->setKind($zonekind);
$zone->setName($zonename);
$createOptions = array(
'name' => $zonename,
'kind' => $zonekind,
);
$nameservers = array();
foreach($_POST['nameserver'] as $ns) {
if (isset($ns) && !empty($ns)) {
array_push($nameservers, $ns);
}
}
if ($zonekind != "Slave") {
if (!isset($_POST['zone']) or isset($_POST['owns'])) {
foreach ($_POST['nameserver'] as $ns) {
$zone->addNameserver($ns);
$createOptions['nameservers'] = $nameservers;
if (!isset($_POST['zone'])) {
if (0 == count($nameservers)) {
jtable_respond(null, 'error', "Require nameservers");
}
} else {
$zone->importData($_POST['zone']);
$createOptions['zone'] = $_POST['zone'];
}
if (isset($defaults['soa_edit_api'])) {
$zone->setSoaEditApi($defaults['soa_edit_api'], True);
$createOptions['soa_edit_api'] = $defaults['soa_edit_api'];
}
if (isset($defaults['soa_edit'])) {
$zone->setSoaEdit($defaults['soa_edit']);
$createOptions['soa_edit'] = $defaults['soa_edit'];
}
} else { // Slave
if (isset($_POST['masters'])) {
foreach (preg_split('/[,;\s]+/', $_POST['masters'], null, PREG_SPLIT_NO_EMPTY) as $master) {
$zone->addMaster($master);
}
$createOptions['masters'] = preg_split('/[,;\s]+/', $_POST['masters'], null, PREG_SPLIT_NO_EMPTY);
}
if (0 == count($createOptions['masters'])) {
jtable_respond(null, 'error', "Slave requires master servers");
}
}
// only admin user and original account can "recreate" zones that are already
// only admin user and original owner can "recreate" zones that are already
// present in our own db but got lost in pdns.
if (!is_adminuser() && get_sess_user() !== get_zone_account($zonename, get_sess_user())) {
if (!is_adminuser() && get_sess_user() !== get_zone_owner($zonename, get_sess_user())) {
jtable_respond(null, 'error', 'Zone already owned by someone else');
}
$api->savezone($zone->export());
$zone = zones_api_request($createOptions);
$zonename = $zone['name'];
$zone = new Zone();
$zone->parse($api->loadzone($zonename));
$zonename = $zone->name;
if (is_adminuser() && isset($_POST['account'])) {
add_db_zone($zonename, $_POST['account']);
$zone->setAccount($_POST['account']);
if (is_adminuser() && isset($_POST['owner'])) {
add_db_zone($zonename, $_POST['owner']);
} else {
add_db_zone($zonename, get_sess_user());
$zone->setAccount(get_sess_user());
}
if (isset($_POST['template']) && $_POST['template'] != 'None') {
@ -322,192 +505,137 @@ case "create":
if ($template['name'] !== $_POST['template']) continue;
foreach ($template['records'] as $record) {
$rrset = $zone->getRRSet($record['name'], $record['type']);
if ($rrset) {
$rrset->delete();
if ($record['type'] == 'NS' and array_search($record['content'], $nameservers) !== FALSE) {
continue;
}
if (isset($record['label'])) {
$record['name'] = $record['label'];
unset($record['label']);
}
create_record($zone, $record);
}
$api->savezone($zone->export());
foreach ($template['records'] as $record) {
if ($record['type'] == 'NS') continue;
$name = $record['name'] != '' ? join(Array($record['name'],'.',$zonename)) : $zonename;
$record['content'] = str_replace("[zonename]", $zonename, $record['content']);
$zone->addRecord($name, $record['type'], $record['content']);
}
break;
}
}
$zone = $api->savezone($zone->export());
writelog("Created zone ".$zone['name']);
if (isset($_POST['zone']) && isset($_POST['owns']) && $_POST['owns'] && count($nameservers)) {
$records = array();
foreach ($nameservers as $ns) {
array_push($records, array('type' => 'NS', 'content' => $ns));
}
update_records($zone, $records[0], $records);
}
unset($zone['records']);
unset($zone['comments']);
jtable_respond($zone, 'single');
break;
case "update":
$zone = new Zone();
$zone->parse($api->loadzone($_POST['id']));
if ($zone->setSoaEditApi($defaults['soa_edit_api']) != False)
writelog("Set SOA-EDIT-API to ".$defaults['soa_edit_api']." for ",$zone->name);
$zoneaccount = isset($_POST['account']) ? $_POST['account'] : $zone->account;
$zone = get_zone_by_id(isset($_POST['id']) ? $_POST['id'] : '');
if ($zone->account !== $zoneaccount) {
$zoneowner = isset($_POST['owner']) ? $_POST['owner'] : $zone['owner'];
if ($zone['owner'] !== $zoneowner) {
if (!is_adminuser()) {
header("Status: 403 Access denied");
jtable_respond(null, 'error', "Can't change account");
jtable_respond(null, 'error', "Can't change owner");
} else {
add_db_zone($zone->name, $zoneaccount);
$zone->setAccount($zoneaccount);
add_db_zone($zone['name'], $zoneowner);
$zone['owner'] = $zoneowner;
}
}
$update = false;
if (isset($_POST['masters'])) {
$zone->eraseMasters();
foreach(preg_split('/[,;\s]+/', $_POST['masters'], null, PREG_SPLIT_NO_EMPTY) as $master) {
$zone->addMaster($master);
}
$zone['masters'] = preg_split('/[,;\s]+/', $_POST['masters'], null, PREG_SPLIT_NO_EMPTY);
$update = true;
}
writelog("Updated zone ".$zone->name);
jtable_respond($api->savezone($zone->export()), 'single');
if ($update) {
$zoneUpdate = $zone;
unset($zoneUpdate['id']);
unset($zoneUpdate['url']);
unset($zoneUpdate['owner']);
$newZone = api_request($zone['url'], $zoneUpdate, 'PUT');
$newZone['owner'] = $zone['owner'];
} else {
$newZone = $zone;
}
unset($newZone['records']);
unset($newZone['comments']);
jtable_respond($newZone, 'single');
break;
case "delete":
$zone = get_zone_by_id(isset($_POST['id']) ? $_POST['id'] : '');
api_request($zone['url'], array(), 'DELETE');
delete_db_zone($zone['name']);
jtable_respond(null, 'delete');
break;
case "listrecords":
$zone = get_zone_by_url(isset($_GET['zoneurl']) ? $_GET['zoneurl'] : '');
$a = api_request($zone['url']);
$records = $a['records'];
foreach ($records as &$record) {
$record['id'] = json_encode($record);
}
unset($record);
usort($records, "record_compare");
jtable_respond($records);
break;
case "createrecord":
$zone = new Zone();
$zone->parse($api->loadzone($_GET['zoneid']));
if ($zone->setSoaEditApi($defaults['soa_edit_api']) != False)
writelog("Set SOA-EDIT-API to ".$defaults['soa_edit_api']." for ",$zone->name);
$zone = get_zone_by_url(isset($_GET['zoneurl']) ? $_GET['zoneurl'] : '');
$record = create_record($zone, $_POST);
$name = isset($_POST['name']) ? $_POST['name'] : '';
$type = $_POST['type'];
$content = $_POST['content'];
if ('' == $name) {
$name = $zone->name;
} elseif (string_ends_with($name, '.')) {
# "absolute" name, shouldn't append zone[name] - but check.
if (!string_ends_with($name, $zone->name)) {
jtable_respond(null, 'error', "Name $name not in zone ".$zone->name);
}
} else if (!string_ends_with($name.'.', $zone->name)) {
$name = $name . '.' . $zone->name;
} else {
$name = $name.'.';
}
if (!_valid_label($name)) {
jtable_respond(null, 'error', "Please only use [a-z0-9_/.-]");
}
if (!$type) {
jtable_respond(null, 'error', "Require a type");
}
if (!is_ascii($content)) {
jtable_respond(null, 'error', "Please only use ASCII-characters in your fields");
}
if (array_search($type, $quoteus) !== FALSE) {
$content = quote_content($content);
}
$record = $zone->addRecord($name, $type, $content, $_POST['disabled'], $_POST['ttl'], $_POST['setptr']);
$api->savezone($zone->export());
writelog("Created record: ".$record['id']);
$record['id'] = json_encode($record);
jtable_respond($record, 'single');
break;
case "editrecord":
$zone = new Zone();
$zone->parse($api->loadzone($_GET['zoneid']));
if ($zone->setSoaEditApi($defaults['soa_edit_api']) != False)
writelog("Set SOA-EDIT-API to ".$defaults['soa_edit_api']." for ",$zone->name);
$zone = get_zone_by_url(isset($_GET['zoneurl']) ? $_GET['zoneurl'] : '');
$old_record = decode_record_id(isset($_POST['id']) ? $_POST['id'] : '');
$rrset = $zone->getRRSet($old_record['name'], $old_record['type']);
$rrset->deleteRecord($old_record['content']);
$records = get_records_except($zone, $old_record);
$content = $_POST['content'];
$type = $_POST['type'];
if (array_search($type, $quoteus) !== FALSE) {
$content = quote_content($content);
$record = make_record($zone, $_POST);
if ($record['name'] !== $old_record['name'] || $record['type'] !== $old_record['type']) {
# rename or retype:
$newRecords = get_records_by_name_type($zone, $record['name'], $record['type']);
array_push($newRecords, $record);
update_records($zone, $old_record, $records); # remove from old list
update_records($zone, $record, $newRecords); # add to new list
} else {
array_push($records, $record);
update_records($zone, $record, $records);
}
$zone->addRecord($_POST['name'], $_POST['type'], $content, $_POST['disabled'], $_POST['ttl'], $_POST['setptr']);
$api->savezone($zone->export());
$record = $zone->getRecord($_POST['name'], $_POST['type'], $content);
writelog("Updated record ".$_POST['id']." to ".$record['id']);
$record['id'] = json_encode($record);
jtable_respond($record, 'single');
break;
case "deleterecord":
$zone = new Zone();
$zone->parse($api->loadzone($_GET['zoneid']));
if ($zone->setSoaEditApi($defaults['soa_edit_api']) != False)
writelog("Set SOA-EDIT-API to ".$defaults['soa_edit_api']." for ",$zone->name);
$zone = get_zone_by_url(isset($_GET['zoneurl']) ? $_GET['zoneurl'] : '');
$old_record = decode_record_id(isset($_POST['id']) ? $_POST['id'] : '');
$rrset = $zone->getRRSet($old_record['name'], $old_record['type']);
$rrset->deleteRecord($old_record['content']);
$api->savezone($zone->export());
$records = get_records_except($zone, $old_record);
writelog("Deleted record ".$_POST['id']);
update_records($zone, $old_record, $records);
jtable_respond(null, 'delete');
break;
case "export":
writelog("Exported zone ".$_GET['zoneid']);
jtable_respond($api->exportzone($_GET['zoneid']), 'single');
break;
$zone = $_GET['zone'];
$export = api_request("/servers/${apisid}/zones/${zone}/export");
case "clone":
$name = $_POST['destname'];
$src = $_POST['sourcename'];
if (!string_ends_with($name, '.')) {
$name = $name.".";
}
if (!_valid_label($name)) {
jtable_respond(null, 'error', "Invalid destination zonename");
}
$srczone = new Zone();
$srczone->parse($api->loadzone($src));
if ($srczone->setSoaEditApi($defaults['soa_edit_api']) != False)
writelog("Set SOA-EDIT-API to ".$defaults['soa_edit_api']." for ",$srczone->name);
$srczone->setId('');
$srczone->setName($name);
$srczone->setSerial('');
$srczone->setKind($_POST['kind']);
$zone = $api->savezone($srczone->export());
$srczone->parse($zone);
foreach ($srczone->rrsets as $rrset) {
$newname = $rrset->name;
$newname = preg_replace('/'.$src.'$/', $name, $newname);
$rrset->setName($newname);
}
if (is_adminuser() && isset($_POST['account'])) {
add_db_zone($name, $_POST['account']);
$srczone->setAccount($_POST['account']);
} else {
add_db_zone($name, get_sess_user());
$srczone->setAccount(get_sess_user());
}
$zone = $api->savezone($srczone->export());
writelog("Cloned zone $src into $name");
jtable_respond($zone, 'single');
jtable_respond($export, 'single');
break;
case "gettemplatenameservers":
@ -541,26 +669,7 @@ case "getformnameservers":
}
}
break;
case "formzonelist":
$zones = $api->listzones();
usort($zones, "zone_compare");
$ret = array();
foreach ($zones as $zone) {
if (!check_account($zone))
continue;
if ($zone['kind'] == 'Slave')
continue;
array_push($ret, array(
'DisplayText' => $zone['name'],
'Value' => $zone['id']));
}
jtable_respond($ret, 'options');
break;
default:
jtable_respond(null, 'error', 'No such action');
break;
}
} catch (Exception $e) {
jtable_respond(null, 'error', $e->getMessage());
}