Added PUT /domains/{domainId}/soa

This commit is contained in:
Lukas Metzger 2018-03-26 19:32:32 +02:00
parent 01cd32e27c
commit 28c0b0d08d
7 changed files with 306 additions and 3 deletions

View file

@ -156,4 +156,50 @@ class Domains
return $res->withJson(['error' => 'Domain is not a slave zone'], 405);
}
}
public function putSoa(Request $req, Response $res, array $args)
{
$userId = $req->getAttribute('userId');
$domainId = $args['domainId'];
$ac = new \Operations\AccessControl($this->c);
if (!$ac->canAccessDomain($userId, $domainId)) {
$this->logger->info('Non admin user tries to get domain without permission.');
return $res->withJson(['error' => 'You have no permissions for this domain.'], 403);
}
$body = $req->getParsedBody();
if (!array_key_exists('primary', $body) ||
!array_key_exists('email', $body) ||
!array_key_exists('refresh', $body) ||
!array_key_exists('retry', $body) ||
!array_key_exists('expire', $body) ||
!array_key_exists('ttl', $body)) {
$this->logger->debug('One of the required fields is missing');
return $res->withJson(['error' => 'One of the required fields is missing'], 422);
}
$soa = new \Operations\Soa($this->c);
try {
$soa->setSoa(
intval($domainId),
$body['email'],
$body['primary'],
intval($body['refresh']),
intval($body['retry']),
intval($body['expire']),
intval($body['ttl'])
);
return $res->withStatus(204);
} catch (\Exceptions\NotFoundException $e) {
$this->logger->warning('Trying to set soa for not existing domain.', ['domainId' => $domainId]);
return $res->withJson(['error' => 'No domain found for id ' . $domainId], 404);
} catch (\Exceptions\SemanticException $e) {
$this->logger->warning('Trying to set soa for slave domain.', ['domainId' => $domainId]);
return $res->withJson(['error' => 'SOA can not be set for slave domains'], 405);
}
}
}

View file

@ -0,0 +1,188 @@
<?php
namespace Operations;
require '../vendor/autoload.php';
/**
* This class provides functions for retrieving and modifying soa records.
*/
class Soa
{
/** @var \Monolog\Logger */
private $logger;
/** @var \PDO */
private $db;
/** @var \Slim\Container */
private $c;
public function __construct(\Slim\Container $c)
{
$this->logger = $c->logger;
$this->db = $c->db;
$this->c = $c;
}
/**
* Get a list of domains according to filter criteria
*
* @param $domainId Domain to update soa
* @param $mail Mail of zone master
* @param $primary The primary nameserver
* @param $refresh The refresh interval
* @param $retry The retry interval
* @param $expire The expire timeframe
* @param $ttl The zone ttl
*
* @return void
*
* @throws NotFoundException If the given domain does not exist
*/
public function setSoa(int $domainId, string $mail, string $primary, int $refresh, int $retry, int $expire, int $ttl)
{
$this->db->beginTransaction();
$query = $this->db->prepare('SELECT id,name,type FROM domains WHERE id=:id');
$query->bindValue(':id', $domainId);
$query->execute();
$record = $query->fetch();
if ($record === false) {
$this->db->rollBack();
throw new \Exceptions\NotFoundException();
} elseif ($record['type'] === 'SLAVE') {
$this->db->rollBack();
throw new \Exceptions\SemanticException();
} else {
$domainName = $record['name'];
}
//Generate soa content string without serial
$soaArray = [
$primary,
$this->fromEmail($mail),
'serial',
$refresh,
$retry,
$expire,
$ttl
];
$query = $this->db->prepare('SELECT content FROM records WHERE domain_id=:id AND type=\'SOA\'');
$query->bindValue(':id', $domainId);
$query->execute();
$content = $query->fetch();
if ($content === false) { //No soa exists yet
$soaArray[2] = strval($this->calculateSerial(0));
$soaString = implode(' ', $soaArray);
$changeDate = strval(time());
$query = $this->db->prepare('
INSERT INTO records (domain_id, name, type, content, ttl, change_date)
VALUES (:domainId, :name, \'SOA\', :content, :ttl, :changeDate)
');
$query->bindValue(':domainId', $domainId, \PDO::PARAM_INT);
$query->bindValue(':name', $domainName, \PDO::PARAM_STR);
$query->bindValue(':content', $soaString, \PDO::PARAM_STR);
$query->bindValue(':ttl', $ttl, \PDO::PARAM_STR);
$query->bindValue(':changeDate', $changeDate, \PDO::PARAM_INT);
$query->execute();
} else {
$oldSerial = intval(explode(' ', $content['content'])[2]);
$soaArray[2] = strval($this->calculateSerial($oldSerial));
$soaString = implode(' ', $soaArray);
$changeDate = strval(time());
$query = $this->db->prepare('UPDATE records SET content=:content, ttl=:ttl,
change_date=:changeDate WHERE domain_id=:domainId AND type=\'SOA\'');
$query->bindValue(':domainId', $domainId, \PDO::PARAM_INT);
$query->bindValue(':content', $soaString, \PDO::PARAM_STR);
$query->bindValue(':ttl', $ttl, \PDO::PARAM_STR);
$query->bindValue(':changeDate', $changeDate, \PDO::PARAM_INT);
$query->execute();
}
$this->db->commit();
}
/**
* Increases the serial number of the given domain to the next required.
*
* If domain has no present soa record this method does nothing.
*
* @param $domainId Domain to update
*
* @return void
*/
public function updateSerial(int $domainId) : void
{
$query = $this->db->prepare('SELECT content FROM records WHERE domain_id=:id AND type=\'SOA\'');
$query->bindValue(':id', $domainId);
$query->execute();
$content = $query->fetch();
if ($content === false) {
$this->logger->warning('Trying to update serial of domain without soa set it first', ['domainId' => $domainId]);
return;
}
$soaArray = explode(' ', $content['content']);
$soaArray[2] = strval($this->calculateSerial(intval($soaArray[2])));
$soaString = implode(' ', $soaArray);
$query = $this->db->prepare('UPDATE records SET content=:content WHERE domain_id=:domainId AND type=\'SOA\'');
$query->bindValue(':content', $soaString, \PDO::PARAM_STR);
$query->bindValue(':domainId', $domainId, \PDO::PARAM_INT);
$query->execute();
}
/**
* Calculate new serial from old
*
* @param $oldSerial Old serial number
*
* @return int New serial number
*/
private function calculateSerial(int $oldSerial) : int
{
$time = new \DateTime(null, new \DateTimeZone('UTC'));
$currentTime = intval($time->format('Ymd')) * 100;
return \max($oldSerial + 1, $currentTime);
}
/**
* Convert email to soa mail string
*
* @param $email Email address
*
* @return string Soa email address
*/
private function fromEmail(string $email)
{
$parts = explode('@', $email);
$parts[0] = str_replace('.', '\.', $parts[0]);
$parts[] = '';
return rtrim(implode(".", $parts), ".");
}
/**
* Convert soa mail to mail string
*
* @param $soaMail Soa email address
*
* @return string Email address
*/
private function toEmail(string $soaEmail)
{
$tmp = preg_replace('/([^\\\\])\\./', '\\1@', $soaEmail, 1);
$tmp = preg_replace('/\\\\\\./', ".", $tmp);
$tmp = preg_replace('/\\.$/', "", $tmp);
return $tmp;
}
}

View file

@ -32,6 +32,8 @@ $app->group('/v1', function () {
$this->delete('/domains/{domainId}', '\Controllers\Domains:delete');
$this->get('/domains/{domainId}', '\Controllers\Domains:getSingle');
$this->put('/domains/{domainId}', '\Controllers\Domains:put');
$this->put('/domains/{domainId}/soa', '\Controllers\Domains:putSoa');
})->add('\Middlewares\Authentication');
});

View file

@ -14,7 +14,7 @@ return [
'dbname' => '$DBNAME'
],
'logging' => [
'level' => 'warning',
'level' => 'error',
'path' => '../../test/logfile.log'
],
'authentication' => [

View file

@ -0,0 +1,67 @@
const test = require('../testlib');
test.run(async function () {
await test('admin', async function (assert, req) {
//Try to set soa for non exitent domain
var res = await req({
url: '/domains/100/soa',
method: 'put',
data: {
primary: 'ns1.example.com',
email: 'hostmaster@example.com',
refresh: 3600,
retry: 900,
expire: 604800,
ttl: 86400
}
});
assert.equal(res.status, 404, 'Updating SOA for not existing domain should fail');
//Try to set soa for slave domain
var res = await req({
url: '/domains/2/soa',
method: 'put',
data: {
primary: 'ns1.example.com',
email: 'hostmaster@example.com',
refresh: 3600,
retry: 900,
expire: 604800,
ttl: 86400
}
});
assert.equal(res.status, 405, 'Updating SOA for slave domain should fail');
//Try to set soa with missing fields
var res = await req({
url: '/domains/2/soa',
method: 'put',
data: {
primary: 'ns1.example.com',
retry: 900,
expire: 604800,
ttl: 86400
}
});
assert.equal(res.status, 422, 'Updating SOA with missing fields should fail.');
//Set soa for zone without one
var res = await req({
url: '/domains/1/soa',
method: 'put',
data: {
primary: 'ns1.example.com',
email: 'hostmaster@example.com',
refresh: 3600,
retry: 900,
expire: 604800,
ttl: 86400
}
});
assert.equal(res.status, 204, 'Updating SOA for Zone without one should succeed.');
});
});

View file

@ -2,7 +2,7 @@ const test = require('../testlib');
const cartesianProduct = require('cartesian-product');
test.run(async function () {
test('admin', async function (assert, req) {
await test('admin', async function (assert, req) {
//GET /domains?page=5&pagesize=10&query=foo&sort=id-asc,name-desc,type-asc,records-asc&type=MASTER
//Test sorting in all combinations

View file

@ -1,7 +1,7 @@
const test = require('../testlib');
test.run(async function () {
test('admin', async function (assert, req) {
await test('admin', async function (assert, req) {
//Try to login with invalid username and password
var res = await req({
url: '/sessions',