Compare commits

...

67 commits

Author SHA1 Message Date
Petr Bena
79f283bdcb generic note 2024-04-17 10:34:25 +02:00
Petr Bena
75ebf21481
Update README.md 2024-01-10 13:37:43 +01:00
Petr Bena
fe8e330cc3 use new value when creating PTR 2023-12-19 11:44:02 +01:00
Petr Bena
a7a383f109 compatibility with older PHP 2023-12-18 13:37:59 +01:00
Petr Bena
1c5abafcbc updated year 2023-12-18 13:11:39 +01:00
Petr Bena
63c0835625 missing implementation of replace_record 2023-12-18 13:07:47 +01:00
Petr Bena
74226bb2e0 new config option -> default ttl per domain 2023-07-03 10:26:45 +02:00
Petr Bena
b55e907e1b use action=new instead of edit for new record
otherwise there are warnings for undefined variables produced with
debugging enabled
2023-01-02 11:16:09 +01:00
Henrik Nordstrom
c018918d8f Add new icon in record list
easier to find than navigating to the New/edit record tab
2023-01-02 11:12:18 +01:00
Petr Bena
d0164f3116 moved custom user config include
I think it needs to be included after session initialization, otherwise
the $_SESSION will not work as expected
2022-07-08 10:50:01 +02:00
Petr Bena
ccfb9721a9 updated PSF
previous commit changed it to some older version, just to make sure...
2022-07-08 10:45:28 +02:00
Henrik Nordström
d4729052a5
Adds the option of per-user configuration files (#16)
* Per user config file
2022-07-08 10:44:47 +02:00
Petr Bena
cd6667c001 logo 2021-05-06 11:46:30 +02:00
Petr Bena
16ccf617aa logo 2021-04-23 12:13:40 +02:00
Petr Bena
9a62da39fe favicon 2021-04-23 11:44:18 +02:00
Petr Bena
b7d6d176cd favicon 2021-04-23 11:39:07 +02:00
Petr Bena
97b02fd6d4 fixed bug in disabled combo box
it seems that disabled elements do not submit data in form fields
2021-01-26 09:40:26 +01:00
Petr Bena
5dd2ead831 insert record name to confirmation dialog 2021-01-22 14:27:49 +01:00
Petr Bena
687f08dd55 updated psf 2021-01-22 14:25:58 +01:00
Petr Bena
19cd0b903a favicon from commons 2020-10-19 23:46:22 +02:00
Petr Bena
977848d443 return small switcher for zone list 2020-10-06 21:08:28 +02:00
Petr Bena
d990a3c7c4 improved ui 2020-10-06 21:07:01 +02:00
Petr Bena
884ff4e597 remember TTL value 2020-09-10 10:25:19 +02:00
Petr Bena
a591e8d74f fixed typo 2020-09-09 09:08:08 +02:00
Petr Bena
0b3f9b3c79 github https 2020-09-07 09:56:39 +02:00
Petr Bena
9b8106163d version++ 2020-09-07 09:55:53 +02:00
Petr Bena
ab2e9ecb11 version fix 2020-09-01 11:51:32 +02:00
Petr Bena
c6a7169eb6 mktar bug 2020-09-01 11:50:00 +02:00
Petr Bena
87cb572011 fixed mktar 2020-09-01 11:49:27 +02:00
Petr Bena
bfbc7f7b0f implemented help 2020-09-01 11:16:32 +02:00
Petr Bena
173cad4358 updated psf 2020-08-27 12:59:30 +02:00
Petr Bena
abe8dc5601 ignore ssl in unit 2020-08-06 15:37:08 +02:00
Petr Bena
a7223ddebc fixed bug
API wasn't properly deleting old record on replace
2020-08-05 16:20:58 +02:00
Petr Bena
b2f8f11c75 fixed 2 bugs
- New feature in API now works for FQDN
- Fixed warning with undefined PTR parameter
2020-08-05 16:17:57 +02:00
Petr Bena
084bd4641b ++version 2020-08-05 16:07:59 +02:00
Petr Bena
c4168adf55 replace_record
this is needed for atomic updates
2020-08-05 16:07:08 +02:00
Petr Bena
0fbef750c5 improved playbooks 2020-07-31 13:19:20 +02:00
Petr Bena
c5faafc01b implemented checks 2020-07-27 13:53:48 +02:00
Petr Bena
30e9a71060 remove examples from tarball 2020-07-27 13:28:48 +02:00
Petr Bena
1729a193cc ansible 2020-07-27 13:28:07 +02:00
Petr Bena
d42505d38a made value field proportionally wider 2020-06-18 10:24:58 +02:00
Petr Bena
da090d1de5 UI tweak
only show delete all trash if we have PTR zone
2020-06-09 15:03:52 +02:00
Petr Bena
d05b1647c8 Merge branch 'master' of github.com:benapetr/dnsphpadmin 2020-06-09 14:30:22 +02:00
Petr Bena
8d3f3b6610 unit test 2020-06-09 14:30:17 +02:00
Petr Bena
c46fbc2735 implemented option to delete associated PTR record 2020-06-09 14:25:08 +02:00
Petr Bena
2a6159ea4e logout now has icon instead of text 2020-06-09 14:01:01 +02:00
Petr Bena
981a92c226 readme update 2020-06-04 10:19:24 +02:00
Petr Bena
9b420f3a8a updated tests 2020-06-02 11:54:43 +02:00
Petr Bena
bab98aa927 implemented option to change PTR when modifying record 2020-06-01 13:06:06 +02:00
Petr Bena
a889cede10 implemented optional PTR delete for API call 2020-06-01 11:20:52 +02:00
Petr Bena
c5717a10d4 API: implemented option to create rev 2020-05-28 16:38:48 +02:00
Petr Bena
b89b2bfe44 count hidden types only if they are hidden 2020-05-27 14:07:07 +02:00
Petr Bena
cbb18e8146 fixed bug in psf 2020-05-27 14:04:04 +02:00
Petr Bena
10498390c8 implement record counter 2020-05-27 13:24:26 +02:00
Petr Bena
c0247d7b1a reorganized code 2020-05-26 21:54:59 +02:00
Petr Bena
c32cbf2c7a redesigned warning flow 2020-05-25 14:56:38 +02:00
Petr Bena
5c13dc09f9 fixed wrong notification functions 2020-05-25 14:36:48 +02:00
Petr Bena
0dccc44ad4 implemented mechanism to display API warnings 2020-05-25 14:27:24 +02:00
Petr Bena
ccc264ed12 mktar fix 2020-05-25 14:06:34 +02:00
Petr Bena
474e9a1549 more API test 2020-05-25 13:21:22 +02:00
Petr Bena
2564cd90e1 improved API unit test 2020-05-25 13:05:31 +02:00
Petr Bena
661d139bfc API error improved 2020-05-20 10:54:02 +02:00
Petr Bena
442295c2cf sanitize user input 2020-05-20 10:38:00 +02:00
Petr Bena
625eec3456 version++ minor 2020-05-20 10:22:38 +02:00
Petr Bena
03f8863e9e validator 2020-05-20 10:21:08 +02:00
Petr Bena
ee057b7fc9 fixed validator 2020-05-19 16:21:14 +02:00
Petr Bena
67d50c5e7e strict hostname checks 2020-05-19 16:17:26 +02:00
28 changed files with 819 additions and 166 deletions

View file

@ -7,10 +7,11 @@ DNS admin panel, designed to operate via nsupdate, for all kinds of RFC complian
* Different servers for querying zone info (transfer) and for update, useful for load balancing
* Audit logs
* Support LDAP / Active Directory authentication
* Web API
* Individual user and LDAP group permissions to edit zones via roles (read/write)
# How does it work
DNS PHP admin is a very simple GUI utility that helps sysadmins manage their DNS records and also provides easy to use interface for end users, which is more idiot friendly than low level command line tools that are typically used to manage BIND9 servers.
DNS PHP admin is a very simple GUI utility that helps sysadmins manage their DNS records and also provides easy to use interface for end users, which is more user friendly than low level command line tools that are typically used to manage BIND9 servers.
It also makes it possible to centralize management of multiple separate DNS servers, so that you can edit multiple zones on multiple different DNS servers.
@ -23,10 +24,10 @@ Then, download release tarball into any folder which is configured a http root o
```
cd /tmp
wget https://github.com/benapetr/dnsphpadmin/releases/download/1.7.0/dnsphpadmin_1.7.0.tar.gz
wget https://github.com/benapetr/dnsphpadmin/releases/download/1.10.0/dnsphpadmin_1.10.0.tar.gz
cd /var/www/html
tar -xf /tmp/dnsphpadmin_1.7.0.tar.gz
mv dnsphpadmin_1.7.0 dnsphpadmin
tar -xf /tmp/dnsphpadmin_1.10.0.tar.gz
mv dnsphpadmin_1.10.0 dnsphpadmin
cd dnsphpadmin
# Now copy the default config file
@ -37,4 +38,10 @@ vi config.php
Now update `$g_domains` so that it contains information about zones you want to manage. Web server must have nsupdate and dig Linux commands installed in paths that are in config.php and it also needs to have firewall access to perform zone transfer and to perform nsupdate updates.
## Docker image
There is also a docker image maintained by Eugene Taylashev
* GitHub: https://github.com/eugene-taylashev/docker-dnsphpadmin
* Docker Hub: https://hub.docker.com/repository/docker/etaylashev/dnsphpadmin
**IMPORTANT:** DNS tool doesn't use any authentication by default, so everyone with access to web server will have access to DNS tool. If this is just a simple setup for 1 or 2 admins who should have unlimited access to everything, you should setup login via htaccess or similar see https://httpd.apache.org/docs/2.4/howto/auth.html for apache. If you have LDAP (active directory is also LDAP), you can configure this tool to use LDAP authentication as well.

230
api.php
View file

@ -17,9 +17,11 @@ require("config.default.php");
require("config.php");
require_once("psf/psf.php");
require_once("includes/common.php");
require_once("includes/debug.php");
require_once("includes/fatal_api.php");
require_once("includes/record_list.php");
require_once("includes/modify.php");
require_once("includes/notifications.php");
require_once("includes/login.php");
require_once("includes/validator.php");
require_once("includes/zones.php");
@ -34,8 +36,19 @@ date_default_timezone_set($g_timezone);
function print_result($result)
{
global $api;
$api->PrintObj([ 'result' => $result ]);
global $api, $g_api_warnings, $g_api_errors;
$json = [ 'result' => $result ];
if (!empty($g_api_warnings))
$json['warnings'] = $g_api_warnings;
if (!empty($g_api_errors))
$json['errors'] = $g_api_errors;
$api->PrintObj($json);
}
function api_warning($text)
{
global $g_api_warnings;
$g_api_warnings[] = $text;
}
function print_success()
@ -204,6 +217,19 @@ function get_zone_for_fqdn_or_throw($fqdn)
return $zone;
}
function validate_type_or_throw($type)
{
global $api;
if (!IsValidRecordType($type))
{
$api->ThrowError('Invalid type', "Type $type is not a valid DNS record type");
return false;
}
return true;
}
function api_call_create_record($source)
{
global $api, $g_domains;
@ -213,6 +239,7 @@ function api_call_create_record($source)
$type = get_required_post_get_parameter('type');
$value = get_required_post_get_parameter('value');
$comment = get_optional_post_get_parameter('comment');
$ptr = IsTrue(get_optional_post_get_parameter('ptr'));
$merge_record = true;
if ($zone === NULL)
@ -224,11 +251,8 @@ function api_call_create_record($source)
if (!check_zone_access($zone))
return false;
if (!IsValidRecordType($type))
{
$api->ThrowError('Invalid type', "Type $type is not a valid DNS record type");
if (!validate_type_or_throw($type))
return false;
}
if (!is_numeric($ttl))
{
@ -236,19 +260,129 @@ function api_call_create_record($source)
return false;
}
$record = SanitizeHostname($record);
if (!IsValidHostName($record))
{
$api->ThrowError('Invalid hostname', "Hostname is containing invalid characters");
return false;
}
$n = "server " . $g_domains[$zone]['update_server'] . "\n";
$merged_record = NULL;
if ($merge_record)
{
$n .= ProcessInsertFromPOST($zone, $record, $value, $type, $ttl);
else
$merged_record = $record . "." . $zone;
} else
{
$n .= ProcessInsertFromPOST("" , $record, $value, $type, $ttl);
$merged_record = $record;
}
$n .= "send\nquit\n";
ProcessNSUpdateForDomain($n, $zone);
WriteToAuditFile("create", $merged_record . " " . $ttl . " " . $type . " " . $value, $comment);
if ($merge_record)
WriteToAuditFile("create", $record . "." . $zone . " " . $ttl . " " . $type . " " . $value, $comment);
else
WriteToAuditFile("create", $record . " " . $ttl . " " . $type . " " . $value, $comment);
if ($ptr == true)
{
Debug('PTR record was requested for ' . $merged_record . ' creating one');
if ($type != 'A')
{
api_warning('Requested PTR record was not created: PTR record can be only created when you are inserting A record, you created ' . $type . ' record instead');
} else
{
DNS_InsertPTRForARecord($value, $merged_record, $ttl, $comment);
}
}
print_success();
return true;
}
function api_call_replace_record($source)
{
global $api, $g_domains;
$zone = get_optional_post_get_parameter('zone');
$record = get_required_post_get_parameter('record');
$ttl = get_required_post_get_parameter('ttl');
$type = get_required_post_get_parameter('type');
$new_value = get_required_post_get_parameter('new_value');
$value = get_optional_post_get_parameter('value');
$comment = get_optional_post_get_parameter('comment');
$new_record = get_optional_post_get_parameter('new_record');
$new_type = get_optional_post_get_parameter('new_type');
$ptr = IsTrue(get_optional_post_get_parameter('ptr'));
$merge_record = true;
// Auto-fill optional
if ($new_type === NULL)
$new_type = $type;
if ($new_record === NULL)
$new_record = $record;
if ($zone === NULL)
{
$merge_record = false;
$zone = get_zone_for_fqdn_or_throw($record);
}
if (!check_zone_access($zone))
return false;
if (!validate_type_or_throw($type))
return false;
if (!validate_type_or_throw($new_type))
return false;
if (!is_numeric($ttl))
{
$api->ThrowError('Invalid ttl', "TTL must be a number");
return false;
}
$old = NULL;
$old_record = NULL;
$merged_record = NULL;
if (!$merge_record)
{
$old = $record . ' 0 ' . $type;
$old_record = $record;
$merged_record = $new_record;
} else
{
$old = $record . '.' . $zone . ' 0 ' . $type;
$old_record = $record . '.' . $zone;
$merged_record = $new_record . '.' . $zone;
}
if ($value !== NULL)
$old .= ' ' . $value;
DNS_ModifyRecord($zone, $new_record, $new_value, $new_type, $ttl, $comment, $old, !$merge_record);
if ($ptr)
{
if ($type != 'A' && $new_type != 'A')
{
api_warning("You requested to modify underlying PTR record, but neither new or old record type is A record, ignoring PTR update request");
} else
{
// PTR update was requested, if old type was A, delete it. If new type is A, create it
if ($type == 'A')
{
if ($value === NULL)
api_warning("Old PTR record was not deleted, because parameter value was not provided - so we don't know what to delete");
else
DNS_DeletePTRForARecord($value, $old_record, $comment);
}
if (($new_type === NULL && $type == 'A') || $new_type == 'A')
{
DNS_InsertPTRForARecord($new_value, $merged_record, $ttl, $comment);
}
}
}
print_success();
return true;
@ -259,10 +393,11 @@ function api_call_delete_record($source)
global $api, $g_domains;
$zone = get_optional_post_get_parameter('zone');
$record = get_required_post_get_parameter('record');
$ttl = get_required_post_get_parameter('ttl');
$ttl = 0;
$type = get_required_post_get_parameter('type');
$value = get_optional_post_get_parameter('value');
$comment = get_optional_post_get_parameter('comment');
$ptr = get_optional_post_get_parameter('ptr');
$merge_record = true;
if ($zone === NULL)
@ -274,36 +409,51 @@ function api_call_delete_record($source)
if (!check_zone_access($zone))
return false;
if (!IsValidRecordType($type))
if (!validate_type_or_throw($type))
return false;
$record = SanitizeHostname($record);
if (!IsValidHostName($record))
{
$api->ThrowError('Invalid type', "Type $type is not a valid DNS record type");
return false;
}
if (!is_numeric($ttl))
{
$api->ThrowError('Invalid ttl', "TTL must be a number");
$api->ThrowError('Invalid hostname', "Hostname is containing invalid characters");
return false;
}
// Value is optional, so in order to make nsupdate call more simple, we prefix it with space
$original_value = $value;
if (!psf_string_is_null_or_empty($value))
$value = " " . $value;
else
$value = "";
$n = "server " . $g_domains[$zone]['update_server'] . "\n";
$merged_record = "";
if ($merge_record)
$n .= "update delete " . $record . "." . $zone . " " . $ttl . " " . $type . $value . "\n";
else
$n .= "update delete " . $record . " " . $ttl . " " . $type . $value . "\n";
{
$n .= "update delete " . $record . "." . $zone . " 0 " . $type . $value . "\n";
$merged_record = $record . "." . $zone;
} else
{
$n .= "update delete " . $record . " 0 " . $type . $value . "\n";
$merged_record = $record;
}
$n .= "send\nquit\n";
ProcessNSUpdateForDomain($n, $zone);
WriteToAuditFile("delete", $merged_record . " 0 " . $type . $value, $comment);
if ($merge_record)
WriteToAuditFile("delete", $record . "." . $zone . " " . $ttl . " " . $type . $value, $comment);
else
WriteToAuditFile("delete", $record . " " . $ttl . " " . $type . $value, $comment);
if ($ptr == true)
{
Debug('PTR record deletion was requested for ' . $merged_record);
if ($type != 'A')
{
api_warning('Requested PTR record was not deleted: PTR record can be only deleted when you are changing A record, you deleted ' . $type . ' record instead');
} else
{
DNS_DeletePTRForARecord($original_value, $merged_record, $comment);
}
}
print_success();
return true;
@ -324,6 +474,7 @@ function api_call_get_record($source)
{
global $api;
$record = get_required_post_get_parameter('record');
$record = SanitizeHostname($record);
if (!IsValidHostName($record))
{
$api->ThrowError('Invalid hostname', "Hostname $record is not a valid hostname");
@ -423,7 +574,7 @@ register_api("list_zones", "List all existing zones that you have access to", "L
register_api('list_records', "List all existing records for a specified zone", "List all existing records for a specified zone", "api_call_list_records", true,
[ new PsfApiParameter("zone", PsfApiParameterType::String, "Zone to list records for") ],
[], '?action=list_records&zone=domain.org');
register_api('create_record', 'Create a new DNS record in specified zone', 'Creates a new DNS record in specific zone. Please mind that domain name / zone is appended to record name automatically, ' .
register_api('create_record', 'Creates a new DNS record in specified zone', 'Creates a new DNS record in specific zone. Please mind that domain name / zone is appended to record name automatically, ' .
'so if you want to add test.domain.org, name of key is only test.', 'api_call_create_record', true,
// Required parameters
[ new PsfApiParameter("record", PsfApiParameterType::String, "Record name, if you don't provide zone name explicitly, this should be FQDN"),
@ -431,19 +582,38 @@ register_api('create_record', 'Create a new DNS record in specified zone', 'Crea
new PsfApiParameter("value", PsfApiParameterType::String, "Value of record") ],
// Optional parameters
[ new PsfApiParameter("zone", PsfApiParameterType::String, "Zone to modify, if not specified and record is fully qualified, it's automatically looked up from config file"),
new PsfApiParameter("ptr", PsfApiParameterType::Boolean, "Optionally create PTR record, works only when you are adding A records"),
new PsfApiParameter("comment", PsfApiParameterType::String, "Optional comment for audit logs") ],
// Example call
'?action=create_record&zone=domain.org&record=test&ttl=3600&type=A&value=0.0.0.0');
register_api('delete_record', 'Deletes DNS record(s) in specified zone', 'Deletes DNS record(s) in specific zone. If you don\'t provide value, all records of given type will be deleted.', 'api_call_delete_record', true,
// Required parameters
[ new PsfApiParameter("record", PsfApiParameterType::String, "Record name, if you don't provide zone name explicitly, this should be FQDN"),
new PsfApiParameter("ttl", PsfApiParameterType::Number, "Time to live (seconds)"), new PsfApiParameter("type", PsfApiParameterType::String, "Record type") ],
new PsfApiParameter("type", PsfApiParameterType::String, "Record type") ],
// Optional parameters
[ new PsfApiParameter("zone", PsfApiParameterType::String, "Zone to modify, if not specified and record is fully qualified, it's automatically looked up from config file"),
[ new PsfApiParameter("ttl", PsfApiParameterType::Number, "Time to live (seconds). Please note that nsupdate ignores TTL in delete requests. This parameter exists only for compatiblity reasons and is silently ignored."),
new PsfApiParameter("zone", PsfApiParameterType::String, "Zone to modify, if not specified and record is fully qualified, it's automatically looked up from config file"),
new PsfApiParameter("value", PsfApiParameterType::String, "Value of record. If not provided, all records with given type will be removed."),
new PsfApiParameter("ptr", PsfApiParameterType::Boolean, "Optionally delete PTR record, works only when you are deleting A records"),
new PsfApiParameter("comment", PsfApiParameterType::String, "Optional comment for audit logs") ],
// Example call
'?action=delete_record&zone=domain.org&record=test&ttl=3600&type=A&value=0.0.0.0');
register_api('replace_record', 'Removes old and create a new DNS record in single nsupdate transaction', 'Replaces specific record. Both records must be within same zone, but may be of different type. Note that due to nature of nsupdate, if record you want to replace ' .
'doesn\'t exist, it will not fail. So replace_record on non-existent record will still create a new record.', 'api_call_replace_record', true,
// Required parameters
[ new PsfApiParameter("record", PsfApiParameterType::String, "Name of existing record you want to replace, if you don't provide zone name explicitly, this should be FQDN"),
new PsfApiParameter("type", PsfApiParameterType::String, "Type of current record that you want to replace"),
new PsfApiParameter("ttl", PsfApiParameterType::Number, "Time to live (seconds)"),
new PsfApiParameter("new_value", PsfApiParameterType::String, "Value of new record")],
// Optional parameters
[ new PsfApiParameter("zone", PsfApiParameterType::String, "Zone to modify, if not specified and record is fully qualified, it's automatically looked up from config file"),
new PsfApiParameter("value", PsfApiParameterType::String, "Value of record. If not provided, all records with given type will be removed and replaced with a single new record"),
new PsfApiParameter("new_record", PsfApiParameterType::String, "New record name, if you are not changing name of key, this can be omitted. If you don't provide zone name explicitly, this should be FQDN"),
new PsfApiParameter("new_type", PsfApiParameterType::String, "Type of record, if you are not changing type, this can be omitted."),
new PsfApiParameter("ptr", PsfApiParameterType::Boolean, "Optionally replace associated PTR record, works only when either new, old or both records are A records"),
new PsfApiParameter("comment", PsfApiParameterType::String, "Optional comment for audit logs") ],
// Example call
'?action=replace_record&record=test.zone.org&ttl=3600&type=A&value=0.0.0.0&new_value=2.2.2.2&ptr=true');
register_api('get_zone_for_fqdn', 'Returns zone name for given FQDN', 'Attempts to look up zone name for given FQDN using configuration file of php dns admin using auto-lookup function',
'api_call_get_zone_for_fqdn', false, [ new PsfApiParameter("fqdn", PsfApiParameterType::String, "FQDN") ], [], '?action=get_zone_for_fqdn&fqdn=test.example.org');
register_api('get_record', 'Return single record with specified FQDN', 'Lookup single record from master server responsible for zone that hosts this record', 'api_call_get_record', true,

View file

@ -30,8 +30,10 @@ $g_domains = [ 'example.domain' => [ 'transfer_server' => 'localhost', 'update_s
// 'read_only' => false, // by default false, if true domain will be read only
// 'in_transfer' => false, // if true domain will be marked as "in transfer" which means it's being transfered from one DNS master to another, so the records may not reflect truth
// 'maintenance_note' => 'This domain is being configured now', // maintenance note to display for this domain
// 'note' => 'This zone is very important', // generic note to display for this domain
// 'tsig' => true,
// 'tsig_key' => 'some_key' ] ];
// 'tsig_key' => 'some_key',
// 'ttl' => 3600 ] ]; // Overrides default global TTL for new records
// List of record types that can be edited
// https://en.wikipedia.org/wiki/List_of_DNS_record_types
@ -41,7 +43,7 @@ $g_editable = [ 'A', 'AAAA', 'CNAME', 'DNAME', 'DS', 'NS', 'PTR', 'SRV', 'SSHFP'
// API is not affected by this
$g_hidden_record_types = [ 'NSEC', 'RRSIG' ];
// Default TTL for new DNS records
// Default TTL for new DNS records (can be also specified per zone using ttl key)
$g_default_ttl = 3600;
// Path to executable of dig, you can also use this to specify some dig options for example:
@ -51,6 +53,9 @@ $g_dig = '/usr/bin/dig';
// Path to executable of nsupdate
$g_nsupdate = '/usr/bin/nsupdate';
// If enabled, it will not be possible to work with garbage hostnames not conforming to standards
$g_strict_hostname_checks = true;
// If set to value higher than 0, dig will be retried for N times, this is useful on broken networks with heavy packet loss
$g_retry_on_error = 2;
@ -225,3 +230,6 @@ $g_caching_memcached_expiry = 0;
// You can optionally enable in-cache statistics that can be exported for use with monitoring, such as prometheus, to obtain usage metrics
$g_caching_stats_enabled = false;
// Per-user configuration location
$g_user_config_prefix = null;

View file

@ -13,6 +13,6 @@
if (!defined('G_DNSTOOL_ENTRY_POINT'))
die("Not a valid entry point");
define('G_DNSTOOL_VERSION', '1.8.0');
define('G_DNSTOOL_VERSION', '1.12.0');
define('G_API_ELOGIN', 2);
define('G_HEADER', 'DNS management tool');

4
examples/ansible/README Normal file
View file

@ -0,0 +1,4 @@
This folder contains some example ansible playbooks that modify DNS via API of dns tool using URI module
create_record.yml - very simple implementation of login, record creation and logout
delete_record.yml - removes the same record, contains extra checks that result is success

View file

@ -0,0 +1,46 @@
---
- hosts: localhost
tasks:
- include_vars: vars.yml
- uri:
url: "{{ dns_tool }}/api.php"
validate_certs: no
method: POST
body_format: form-urlencoded
body:
action: login
loginUsername: "{{ dns_tool_username }}"
loginPassword: "{{ dns_tool_password }}"
name: 'Login to DNS tool'
no_log: True
register: login
delegate_to: localhost
failed_when: "login.json.result != 'success'"
- uri:
url: "{{ dns_tool }}/api.php"
validate_certs: no
method: POST
body_format: form-urlencoded
body:
action: create_record
record: "petr.bena.cz.preprod"
ttl: 3600
type: A
value: 1.1.2.2
headers:
Cookie: "{{ login.set_cookie }}"
delegate_to: localhost
name: 'Create a new record'
- uri:
url: "{{ dns_tool }}/api.php"
validate_certs: no
method: POST
body_format: form-urlencoded
body:
action: logout
headers:
Cookie: "{{ login.set_cookie }}"
delegate_to: localhost
name: 'Logout from DNS tool'

View file

@ -0,0 +1,49 @@
---
- hosts: localhost
tasks:
- include_vars: vars.yml
- uri:
url: "{{ dns_tool }}/api.php"
validate_certs: no
method: POST
body_format: form-urlencoded
body:
action: login
loginUsername: "{{ dns_tool_username }}"
loginPassword: "{{ dns_tool_password }}"
name: 'Login to DNS tool'
register: login
nolog: True
delegate_to: localhost
failed_when: "login.json.result != 'success'"
- uri:
url: "{{ dns_tool }}/api.php"
validate_certs: no
method: POST
body_format: form-urlencoded
body:
action: delete_record
record: "petr.bena.cz.preprod"
type: A
value: 1.1.2.2
headers:
Cookie: "{{ login.set_cookie }}"
name: 'Delete record'
delegate_to: localhost
register: this
failed_when: "this.json.result != 'success'"
- uri:
url: "{{ dns_tool }}/api.php"
validate_certs: no
method: POST
body_format: form-urlencoded
body:
action: logout
headers:
Cookie: "{{ login.set_cookie }}"
name: 'Logout from DNS tool'
delegate_to: localhost
register: this
failed_when: "this.json.result != 'success'"

View file

@ -0,0 +1,3 @@
dns_tool: "https://dnstool.org/dns"
dns_tool_username: ""
dns_tool_password: ""

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -151,3 +151,20 @@ function GetCurrentUserName()
return "unknown user";
return $_SERVER['REMOTE_USER'];
}
//! Required to handle various non-standard boolean interpretations, mostly for strings from API requests
function IsTrue($bool)
{
if ($bool === true)
return true;
// Check string version
if ($bool == "true" || $bool == "t")
return true;
// Check int version
if (is_numeric($bool) && $bool != 0)
return true;
return false;
}

View file

@ -14,6 +14,8 @@
if (!defined('G_DNSTOOL_ENTRY_POINT'))
die("Not a valid entry point");
require_once("notifications.php");
function ShowError($form, $txt)
{
$msg = new BS_Alert('FATAL: ' . $txt, 'danger', $form);
@ -29,15 +31,9 @@ function CheckEmpty($form, $label, $name)
return true;
}
//! Insert a warning message to warning message container, right now this works only with UI, it does nothing when used with API calls
function DisplayWarning($text)
{
if (G_DNSTOOL_ENTRY_POINT === "api.php")
return;
global $g_warning_container;
$warning_box = new BS_Alert('<b>WARNING:</b> ' . htmlspecialchars($text), 'warning');
$warning_box->EscapeHTML = false;
$g_warning_container->AppendObject($warning_box);
Notifications::DisplayWarning($text);
}
function GetSwitcher($parent)
@ -63,4 +59,4 @@ function GetSwitcher($parent)
'window.open("index.php?action=manage&domain=" + switcher[0].value, "_self");' .
"}\n";
return $switcher;
}
}

View file

@ -54,7 +54,7 @@ function GetLoginInfo()
{
$role_info = ' (' . psf_string_auto_trim(implode (', ', $g_auth_roles_map[$_SESSION['user']]), 80, '...') . ')';
}
return '<div class="login_info"><span class="glyphicon glyphicon-user"></span>' . $_SESSION["user"] . $role_info . ' <a href="?logout">logout</a></div>';
return '<div class="login_info"><span class="glyphicon glyphicon-user"></span>' . $_SESSION["user"] . $role_info . ' <a href="?logout"><span class="glyphicon glyphicon-log-out" title="logout"></span></a></div>';
}
function ProcessLogin_Error($reason)

View file

@ -19,6 +19,7 @@ require_once("audit.php");
require_once("common.php");
require_once("debug.php");
require_once("nsupdate.php");
require_once("validator.php");
require_once("zones.php");
//! Wrapper around nsupdate from nsupdate.php that checks if there are custom TSIG overrides for given domain
@ -42,34 +43,6 @@ function ProcessNSUpdateForDomain($input, $domain)
return nsupdate($input, $tsig, $tsig_key, $zone_name);
}
function ProcessDelete($well)
{
global $g_domains, $g_selected_domain;
if (!isset($_GET["delete"]))
return;
if (strlen($g_selected_domain) == 0)
Error("No domain");
if (!Zones::IsEditable($g_selected_domain))
Error("Domain $g_selected_domain is not writeable");
if (!IsAuthorizedToWrite($g_selected_domain))
Error("You are not authorized to edit $g_selected_domain");
$record = $_GET["delete"];
if (psf_string_contains($record, "\n"))
Error("Invalid delete string");
$input = "server " . $g_domains[$g_selected_domain]["update_server"] . "\n";
$input .= "update delete " . $record . "\nsend\nquit\n";
ProcessNSUpdateForDomain($input, $g_selected_domain);
WriteToAuditFile("delete", $record);
IncrementStat('delete');
$well->AppendObject(new BS_Alert("Successfully deleted record " . $record));
}
function ProcessInsertFromPOST($zone, $record, $value, $type, $ttl)
{
if (psf_string_is_null_or_empty($record) && psf_string_is_null_or_empty($zone))
@ -87,3 +60,161 @@ function ProcessInsertFromPOST($zone, $record, $value, $type, $ttl)
return "update add " . $fqdn . " " . $ttl . " " . $type . " " . $value . "\n";
}
//! Create a new record in given zone, returns false on error - however, some errors may cancel execution
//! This function may crash the app without returning
function DNS_CreateRecord($zone, $record, $value, $type, $ttl, $comment)
{
global $g_domains;
$input = "server " . $g_domains[$zone]['update_server'] . "\n";
$input .= ProcessInsertFromPOST($zone, $record, $value, $type, $ttl);
$input .= "send\nquit\n";
$result = ProcessNSUpdateForDomain($input, $zone);
if (strlen($result) > 0)
Debug("result: " . $result);
WriteToAuditFile('create', $record . "." . $zone . " " . $ttl . " " . $type . " " . $value, $comment);
IncrementStat('create');
return true;
}
//! Replace record - atomic, returns true on success
function DNS_ModifyRecord($zone, $record, $value, $type, $ttl, $comment, $old, $is_fqdn = false)
{
global $g_domains;
if (!NSupdateEscapeCheck($old))
Error('Invalid data for old record: ' . $old);
$input = "server " . $g_domains[$zone]['update_server'] . "\n";
// First delete the existing record
$input .= "update delete " . $old . "\n";
if ($is_fqdn == false)
$input .= ProcessInsertFromPOST($zone, $record, $value, $type, $ttl);
else
$input .= ProcessInsertFromPOST(NULL, $record, $value, $type, $ttl);
$input .= "send\nquit\n";
$result = ProcessNSUpdateForDomain($input, $zone);
if (strlen($result) > 0)
Debug("result: " . $result);
WriteToAuditFile('replace_delete', $old, $comment);
IncrementStat('replace_delete');
WriteToAuditFile('replace_create', $record . "." . $zone . " " . $ttl . " " . $type . " " . $value, $comment);
IncrementStat('replace_create');
return true;
}
function DNS_DeleteRecord($zone, $record)
{
global $g_domains;
if (strlen($zone) == 0)
Error("No domain");
if (!Zones::IsEditable($zone))
Error("Domain $zone is not writeable");
if (!IsAuthorizedToWrite($zone))
Error("You are not authorized to edit $zone");
if (!NSupdateEscapeCheck($record))
Error("Invalid delete string: " . $record);
$input = "server " . $g_domains[$zone]['update_server'] . "\n";
$input .= "update delete " . $record . "\nsend\nquit\n";
ProcessNSUpdateForDomain($input, $zone);
WriteToAuditFile("delete", $record);
IncrementStat('delete');
return true;
}
//! Try to insert a PTR record for given IP, on failure, warning is emitted and false returned, true returned on success
//! this function is designed as a helper function that is used together with creation of A record, so it's never fatal
function DNS_InsertPTRForARecord($ip, $fqdn, $ttl, $comment)
{
global $g_domains;
Debug('PTR record was requested, checking zone name');
$ip_parts = explode('.', $ip);
if (count($ip_parts) != 4)
{
DisplayWarning('PTR record was not created: record '. $ip .' is not a valid IPv4 quad');
return false;
}
$arpa = $ip_parts[3] . '.' . $ip_parts[2] . '.' . $ip_parts[1] . '.' . $ip_parts[0] . '.in-addr.arpa';
$arpa_zone = Zones::GetZoneForFQDN($arpa);
if ($arpa_zone === NULL)
{
DisplayWarning('PTR record was not created: there is no PTR zone for record '. $ip);
return false;
}
if (!Zones::IsEditable($arpa_zone))
{
DisplayWarning("PTR record was not created for $ip: zone " . $arpa_zone . ' is read only');
return false;
}
if (!IsAuthorizedToWrite($arpa_zone))
{
DisplayWarning("PTR record was not created: you don't have write access to zone " . $arpa_zone);
return false;
}
Debug('Found PTR useable zone: ' . $arpa_zone);
if (!psf_string_endsWith($fqdn, '.'))
$fqdn = $fqdn . '.';
// Let's insert this record
$input = "server " . $g_domains[$arpa_zone]['update_server'] . "\n";
$input .= ProcessInsertFromPOST(NULL, $arpa, $fqdn, 'PTR', $ttl);
$input .= "send\nquit\n";
$result = ProcessNSUpdateForDomain($input, $arpa_zone);
if (strlen($result) > 0)
Debug("result: " . $result);
WriteToAuditFile('create', $arpa . " " . $ttl . " PTR " . $fqdn, $comment);
IncrementStat('create');
return true;
}
//! Try to delete a PTR record for a given IP, on failure, warning is emitted and false returned, true returned on success
//! this function is designed as a helper function that is used together with modifications of A record, so it's never fatal
function DNS_DeletePTRForARecord($ip, $fqdn, $comment)
{
global $g_domains;
Debug('PTR record removal was requested, checking zone name');
$ip_parts = explode('.', $ip);
if (count($ip_parts) != 4)
{
DisplayWarning('PTR record was not deleted: record '. $ip .' is not a valid IPv4 quad');
return false;
}
$arpa = $ip_parts[3] . '.' . $ip_parts[2] . '.' . $ip_parts[1] . '.' . $ip_parts[0] . '.in-addr.arpa';
$arpa_zone = Zones::GetZoneForFQDN($arpa);
if ($arpa_zone === NULL)
{
DisplayWarning('PTR record was not deleted: there is no PTR zone for record '. $ip);
return false;
}
if (!Zones::IsEditable($arpa_zone))
{
DisplayWarning("PTR record was not deleted for $ip: zone " . $arpa_zone . ' is read only');
return false;
}
if (!IsAuthorizedToWrite($arpa_zone))
{
DisplayWarning("PTR record was not deleted: you don't have write access to zone " . $arpa_zone);
return false;
}
Debug('Found PTR useable zone: ' . $arpa_zone);
if (!psf_string_endsWith($fqdn, '.'))
$fqdn = $fqdn . '.';
// Let's insert this record
$input = "server " . $g_domains[$arpa_zone]['update_server'] . "\n";
$input .= "update delete " . $arpa . " 0 PTR " . $fqdn . "\n";
$input .= "send\nquit\n";
$result = ProcessNSUpdateForDomain($input, $arpa_zone);
if (strlen($result) > 0)
Debug("result: " . $result);
WriteToAuditFile('delete', $arpa . " 0 PTR " . $fqdn, $comment);
IncrementStat('delete');
return true;
}

View file

@ -18,20 +18,36 @@ if (!defined('G_DNSTOOL_ENTRY_POINT'))
$g_error_container = new BS_FluidContainer();
$g_warning_container = new BS_FluidContainer();
//! Buffer that contains list of API warnings
$g_api_warnings = [];
$g_api_errors = [];
//! Provides interface to all notification functions that hold and display errors, warnings, etc.
class Notifications
{
public static function DisplayError($text)
public static function DisplayWarning($text)
{
global $g_warning_container;
global $g_warning_container, $g_api_warnings;
if (G_DNSTOOL_ENTRY_POINT === "api.php")
{
// API have separate container as we don't work with HTML there
$g_api_warnings[] = $text;
return;
}
$warning_box = new BS_Alert('<b>WARNING:</b> ' . htmlspecialchars($text), 'warning');
$warning_box->EscapeHTML = false;
$g_warning_container->AppendObject($warning_box);
}
public static function DisplayWarning($text)
public static function DisplayError($text)
{
global $g_error_container;
global $g_error_container, $g_api_errors;
if (G_DNSTOOL_ENTRY_POINT === "api.php")
{
// API have separate container as we don't work with HTML there
$g_api_errors[] = $text;
return;
}
$fatal_box = new BS_Alert('<b>ERROR:</b> ' . $text, 'danger');
$fatal_box->EscapeHTML = false;
$g_error_container->AppendObject($fatal_box);

View file

@ -26,6 +26,10 @@ $g_show_hidden_types = false;
// when rendering UI so that we know if "show / hide" button should be even present or not
$g_hidden_types_present = false;
// Counts of visible items
$g_hidden_records_count = 0;
$g_total_records_count = 0;
function GetStatusOfZoneAsNote($domain)
{
global $g_domains;
@ -64,6 +68,11 @@ function GetStatusOfZoneAsNote($domain)
$is_ok = false;
$status->Text .= '<span class="glyphicon glyphicon-alert"></span> <b>Maintenance note:</b> ' .$domain_info['maintenance_note'];
}
if (array_key_exists('note', $domain_info))
{
$is_ok = false;
$status->Text .= '<span class="glyphicon glyphicon-info"></span> <b>Note:</b> ' .$domain_info['note'];
}
if ($is_ok)
return NULL;
@ -198,7 +207,7 @@ function GetRecordList($zone)
function GetRecordListTable($parent, $domain)
{
global $g_editable, $g_show_hidden_types, $g_hidden_record_types, $g_hidden_types_present;
global $g_editable, $g_show_hidden_types, $g_hidden_record_types, $g_hidden_types_present, $g_total_records_count, $g_hidden_records_count;
$table = new BS_Table($parent);
$table->Condensed = true;
$table->Headers = [ "Record", "TTL", "Scope", "Type", "Value", "Options" ];
@ -207,30 +216,47 @@ function GetRecordListTable($parent, $domain)
$table->SetColumnWidth(5, '80px'); // Options
$records = GetRecordList($domain);
$is_editable = Zones::IsEditable($domain) && IsAuthorizedToWrite($domain);
$has_ptr = Zones::HasPTRZones();
foreach ($records as $record)
{
$g_total_records_count++;
if (in_array($record[3], $g_hidden_record_types))
{
$g_hidden_types_present = true;
if (!$g_show_hidden_types)
{
$g_hidden_records_count++;
continue;
}
}
if (!$is_editable || !in_array($record[3], $g_editable))
{
$record[] = '';
} else
{
$record[] = '<a href="index.php?action=manage&domain=' . $domain . '&delete=' .
urlencode($record[0] . " " . $record[1] . " " . $record[3] . " " . $record[4]) .
'" onclick="return confirm(\'Are you sure?\')"><span class="glyphicon glyphicon-trash" title="Delete"></span></a>&nbsp;&nbsp;&nbsp;&nbsp;' .
'<a href="index.php?action=edit&domain=' . $domain . '&key=' .
$delete_record = '<a href="index.php?action=manage&domain=' . $domain . '&delete=' . urlencode($record[0] . " " . $record[1] . " " . $record[3] . " " . $record[4]) .
'" onclick="return confirm(\'Are you sure you want to delete ' . $record[0] . '?\')"><span class="glyphicon glyphicon-trash" title="Delete"></span></a>';
$delete_record_with_ptr = '';
if ($has_ptr && $record[3] == 'A')
{
// Optional button to delete record together with PTR record, show only if there are PTR zones in cfg
$delete_record_with_ptr = '<a href="index.php?action=manage&ptr=true&key=' . urlencode($record[0]) . '&value=' . urlencode($record[4]) . '&type=' . $record[3] . '&domain=' . $domain .
'&delete=' . urlencode($record[0] . ' ' . $record[1] . " " . $record[3] . " " . $record[4]) .
'" onclick="return confirm(\'Are you sure you want to delete ' . $record[0] . '?\')"><span style="color: #ff0000;" class="glyphicon glyphicon-trash" title="Delete together with associated PTR record (if any exist)"></span></a>';
}
$large_space = '&nbsp;&nbsp;';
$record[] = $delete_record . $large_space . '<a href="index.php?action=edit&domain=' . $domain . '&key=' .
urlencode($record[0]) . "&ttl=" . $record[1] . "&type=" . $record[3] . "&value=" . urlencode($record[4]) .
"&old=" . urlencode($record[0] . " " . $record[1] . " " . $record[3] . " " . $record[4]) .
'"><span title="Edit" class="glyphicon glyphicon-pencil"></span></a>';
'"><span title="Edit" class="glyphicon glyphicon-pencil"></span></a>' . $large_space . $delete_record_with_ptr;
}
$record[4] = '<span class="value">' . $record[4] . '</span>';
$table->AppendRow($record);
}
if ($is_editable) {
$add = '<a href="index.php?action=new&domain=' . $domain . '"><span title="Add New" class="glyphicon glyphicon-plus"></span></a>';
$table->AppendRow(['', '', '', '', '', $add]);
}
return $table;
}

View file

@ -80,7 +80,7 @@ class TabBatch
$form->Method = FormMethod::Post;
$layout = new HtmlTable($form);
$layout->BorderSize = 0;
$dl = new ComboBox("zone", $layout);
$dl = new BS_ComboBox("zone", $layout);
foreach ($g_domains as $key => $info)
{
if (!IsAuthorizedToWrite($key))

View file

@ -15,6 +15,7 @@ if (!defined('G_DNSTOOL_ENTRY_POINT'))
die("Not a valid entry point");
require_once("common_ui.php");
require_once("debug.php");
require_once("validator.php");
require_once("modify.php");
require_once("zones.php");
@ -57,43 +58,47 @@ class TabEdit
if (!is_numeric($ttl))
Error('TTL must be a number');
// Sanitize input from user
$record = SanitizeHostname($record);
if (!IsValidHostName($record))
Error('Invalid hostname: ' . $record);
$input = "server " . $g_domains[$zone]['update_server'] . "\n";
$comment = NULL;
if (isset($_POST["comment"]))
$comment = $_POST["comment"];
if ($_POST['submit'] == 'Create')
{
$input .= ProcessInsertFromPOST($zone, $record, $value, $type, $ttl);
$input .= "send\nquit\n";
$result = ProcessNSUpdateForDomain($input, $zone);
if (strlen($result) > 0)
Debug("result: " . $result);
WriteToAuditFile('create', $record . "." . $zone . " " . $ttl . " " . $type . " " . $value, $comment);
IncrementStat('create');
$form->AppendObject(new BS_Alert("Successfully inserted record " . $record . "." . $zone));
if (DNS_CreateRecord($zone, $record, $value, $type, $ttl, $comment))
$form->AppendObject(new BS_Alert("Successfully inserted record " . $record . "." . $zone));
} else if ($_POST["submit"] == "Edit")
{
if (!isset($_POST["old"]))
Error("Missing old record necessary for update");
if (!NSupdateEscapeCheck($_POST["old"]))
Error('Invalid data for old record: ' . $_POST["old"]);
// First delete the existing record
$input .= "update delete " . $_POST["old"] . "\n";
$input .= ProcessInsertFromPOST($zone, $record, $value, $type, $ttl);
$input .= "send\nquit\n";
$result = ProcessNSUpdateForDomain($input, $zone);
if (strlen($result) > 0)
Debug("result: " . $result);
WriteToAuditFile('replace_delete', $_POST["old"], $comment);
IncrementStat('replace_delete');
WriteToAuditFile('replace_create', $record . "." . $zone . " " . $ttl . " " . $type . " " . $value, $comment);
IncrementStat('replace_create');
$form->AppendObject(new BS_Alert("Successfully replaced " . $_POST["old"] . " with " . $record . "." . $zone . " " .
$ttl . " " . $type . " " . $value));
if (DNS_ModifyRecord($zone, $record, $value, $type, $ttl, $comment, $_POST["old"]))
{
$form->AppendObject(new BS_Alert("Successfully replaced " . $_POST["old"] . " with " . $record . "." . $zone . " " .
$ttl . " " . $type . " " . $value));
}
// Delete PTR if wanted
if (isset($_POST['ptr']) && $_POST['ptr'] === "true")
{
if (isset($_POST["old_type"]) && $_POST["old_type"] == "A")
{
// Check if all necessary values are present
if (!isset($_POST["old_value"]) || !isset($_POST["old_record"]))
{
DisplayWarning("PTR record was not deleted, because old_record or old_value was missing");
} else
{
DNS_DeletePTRForARecord($_POST["old_value"], $_POST["old_record"], $comment);
}
} else
{
Debug("Not removing PTR, original type was " . $_POST["old_type"]);
}
}
} else
{
Error("Unknown modify mode");
@ -102,53 +107,18 @@ class TabEdit
// Create PTR if wanted
if (isset($_POST['ptr']) && $_POST['ptr'] === "true")
{
Debug('PTR record was requested, checking zone name');
if ($type !== "A")
{
DisplayWarning('PTR record was not created: PTR record can be only created when you are inserting A record, you created ' . $type . ' record instead');
return;
}
$ip_parts = explode('.', $value);
if (count($ip_parts) != 4)
{
DisplayWarning('PTR record was not created: record '. $value .' is not a valid IPv4 quad');
return;
}
$arpa = $ip_parts[3] . '.' . $ip_parts[2] . '.' . $ip_parts[1] . '.' . $ip_parts[0] . '.in-addr.arpa';
$arpa_zone = Zones::GetZoneForFQDN($arpa);
if ($arpa_zone === NULL)
{
DisplayWarning('PTR record was not created: there is no PTR zone for record '. $value);
return;
}
if (!Zones::IsEditable($arpa_zone))
{
DisplayWarning('PTR record was not created: zone ' . $arpa_zone . ' is read only');
return;
}
if (!IsAuthorizedToWrite($arpa_zone))
{
DisplayWarning("PTR record was not created: you don't have write access to zone " . $arpa_zone);
return;
}
Debug('Found PTR useable zone: ' . $arpa_zone);
$arpa_value = $record . '.' . $zone . '.';
// Let's insert this record
$input = "server " . $g_domains[$arpa_zone]['update_server'] . "\n";
$input .= ProcessInsertFromPOST(NULL, $arpa, $arpa_value, 'PTR', $ttl);
$input .= "send\nquit\n";
$result = ProcessNSUpdateForDomain($input, $arpa_zone);
if (strlen($result) > 0)
Debug("result: " . $result);
WriteToAuditFile('create', $arpa . " " . $ttl . " PTR " . $arpa_value, $comment);
IncrementStat('create');
DNS_InsertPTRForARecord($value, $record . '.' . $zone, $ttl, $comment);
}
}
public static function GetInsertForm($parent, $edit_mode = false, $default_key = "", $default_ttl = NULL, $default_type = "A", $default_value = "", $default_comment = "")
{
global $g_audit, $g_selected_domain, $g_domains, $g_editable, $g_default_ttl;
global $g_audit, $g_selected_domain, $g_domains, $g_editable;
// In case we are returning to insert form from previous insert, make default type the one we used before
if (isset($_POST['type']))
@ -162,20 +132,24 @@ class TabEdit
if (isset($_POST['comment']))
$default_comment = $_POST['comment'];
if (isset($_POST['ttl']))
$default_ttl = $_POST['ttl'];
// If ttl is not specified use default one from config file
if ($default_ttl === NULL)
$default_ttl = strval($g_default_ttl);
$default_ttl = strval(Zones::GetDefaultTTL($g_selected_domain));
$form = new Form("index.php?action=new", $parent);
$form->Method = FormMethod::Post;
$layout = new HtmlTable($form);
$layout->BorderSize = 0;
$layout->ColWidth[4] = '40%';
$layout->Headers = [ "Record", "Zone", "TTL", "Type", "Value" ];
if ($g_audit)
$layout->Headers[] = 'Comment';
$form_items = [];
$form_items[] = new BS_TextBox("record", $default_key, NULL, $layout);
$dl = new ComboBox("zone", $layout);
$dl = new BS_ComboBox("zone", $layout);
if ($edit_mode)
{
if ($g_selected_domain === NULL)
@ -184,6 +158,8 @@ class TabEdit
}
$dl->AddDefaultValue($g_selected_domain, "." . $g_selected_domain);
$dl->Enabled = false;
// we must add a hidden element that preserves the zone because disabled HTML elements do not submit form data
$form->AppendObject(new Hidden("zone", $g_selected_domain));
} else
{
foreach ($g_domains as $key => $info)
@ -198,7 +174,7 @@ class TabEdit
}
$form_items[] = $dl;
$form_items[] = new BS_TextBox("ttl", $default_ttl, NULL, $layout);
$tl = new ComboBox("type", $layout);
$tl = new BS_ComboBox("type", $layout);
$types = $g_editable;
foreach ($types as $type)
{
@ -219,17 +195,67 @@ class TabEdit
$form_items[] = $comment;
}
$layout->AppendRow($form_items);
if (!$edit_mode && Zones::HasPTRZones())
$form->AppendObject(new BS_CheckBox("ptr", "true", false, NULL, $form, "Create PTR record for this IP (works only with A records)"));
if (Zones::HasPTRZones())
{
if (!$edit_mode)
$form->AppendObject(new BS_CheckBox("ptr", "true", false, NULL, $form, "Create PTR record for this IP (works only with A records)"));
else
$form->AppendObject(new BS_CheckBox("ptr", "true", false, NULL, $form, "Modify underlying PTR records (works only if original, new or both values are A records)"));
}
if (isset($_GET["old"]))
$form->AppendObject(new Hidden("old", htmlspecialchars($_GET["old"])));
$form->AppendObject(new Hidden("old", htmlspecialchars($_GET["old"])));
if ($edit_mode)
{
// Preserve old values, we need to work with them when modifying PTR records
$form->AppendObject(new Hidden("old_record", htmlspecialchars($_GET["key"])));
$form->AppendObject(new Hidden("old_ttl", htmlspecialchars($default_ttl)));
$form->AppendObject(new Hidden("old_type", htmlspecialchars($default_type)));
$form->AppendObject(new Hidden("old_value", htmlspecialchars($default_value)));
$form->AppendObject(new BS_Button("submit", "Edit"));
else
} else
{
$form->AppendObject(new BS_Button("submit", "Create"));
}
return $form;
}
public static function GetHelp()
{
$help = new DivContainer();
$help->AppendLine();
$help->AppendHtmlLine('<a data-toggle="collapse" href="#collapseHelp">Display help</a>');
$c = new DivContainer($help);
$c->ClassName = "collapse";
$c->ID = "collapseHelp";
$c->AppendHeader("Record", 3);
$c->AppendHtmlLine('Name of the key you want to add. If you want to create DNS record <code>test.domain.org</code> in zone domain.org, then value of field record will be just <code>test</code>. <b>Do not append zone name to record name, this is done automatically</b>. Record can be also left blank if you want to add a record for zone apex (zone itself), such as MX records.');
$c->AppendHeader("Zone", 3);
$c->AppendHtmlLine('Name of zone you want to create record in. In case that subzone exist (for example you want to add record <code>subzone.test.domain.org</code> but subzone <code>test.domain.org</code> exists in dropdown menu), you must create the record within the subzone, not in the parent zone, otherwise it will not be visible in domain name system. If no subzone exists, then you can create a record <code>subzone.test</code> inside of <code>domain.org</code>.');
$c->AppendHeader("TTL", 3);
$c->AppendHtmlLine('Time to live tells caching name servers for how long can this record be cached for. Too low TTL may lead to performance issues as the request to resolve such record will be forwarded to authoritative name server most of the time. Too long TTL can make it complicated to change the value of record, because caching name servers will hold the cached value for too long. If you are not sure which value to pick, leave the default value.');
$c->AppendHeader("Type", 3);
$c->AppendHtmlLine('Type of DNS record, following record types are most common:');
$record_types = new BS_Table($c);
$record_types->Headers = [ 'Type', 'Description' ];
$record_types->AppendRow( [ 'A', 'IPv4 record, value of this record is IPv4 address, for example 1.2.3.4' ]);
$record_types->AppendRow( [ 'AAAA', 'IPv6 record, value of this record is IPv6 address, for example ::1' ]);
$record_types->AppendRow( [ 'TXT', 'Text record, must be max 255 characters in length, otherwise you need to split it to multiple parts within quotes ("), each part max. 255 characters in size' ]);
$record_types->AppendRow( [ 'MX', 'Mail server record, value consist of two parts, priority and hostname of mail server, for example: <code>10 mail.domain.org</code>']);
$record_types->AppendRow( [ 'NS', 'Delegates a record to another name server. If used on zone apex it defines authoritative name servers for a zone.']);
$record_types->AppendRow( [ 'SSHFP', 'SSH fingerprint, used by SSH client when verifying that target server has authentic fingerprint']);
$record_types->AppendRow( [ 'CNAME', 'Redirect record to another domain name, this will redirect all record types for given record name and therefore can\'t be used on zone apex']);
$record_types->AppendRow( [ 'SOA', 'Start of authority record - this record exists only for apex of zone and denotes existence of a zone, it includes administrative data for zone, this record is returned twice in zone transfer, as first and last record']);
$c->AppendHtmlLine('See <a href="https://en.wikipedia.org/wiki/List_of_DNS_record_types" target="_blank">https://en.wikipedia.org/wiki/List_of_DNS_record_types</a> for a more complete and detailed list');
$c->AppendHeader("Value", 3);
$c->AppendHtmlLine('Value of record, format depends on record type');
$c->AppendHeader("Comment", 3);
$c->AppendHtmlLine('Optional comment for audit log of DNS tool, this has no effect on DNS server itself. This field is available only if audit subsystem is enabled.');
//$c->AppendObject(new BS_List);
return $help;
}
public static function GetEditForm($parent)
{
global $g_selected_domain;

View file

@ -14,13 +14,49 @@
if (!defined('G_DNSTOOL_ENTRY_POINT'))
die("Not a valid entry point");
require_once("common.php");
require_once("common_ui.php");
require_once("debug.php");
require_once("modify.php");
class TabManage
{
//! Called from index.php this function is supposed to process deleting if requested in UI
public static function ProcessDelete($well)
{
global $g_domains, $g_selected_domain;
if (!isset($_GET["delete"]))
return;
$record = $_GET["delete"];
if (DNS_DeleteRecord($g_selected_domain, $record))
$well->AppendObject(new BS_Alert("Successfully deleted record " . $record));
if (isset($_GET["ptr"]) && $_GET["ptr"] == true)
{
Debug('PTR record deletion was requested for ' . $record);
if (!isset($_GET['key']) || !isset($_GET['value']) || !isset($_GET['type']))
{
Warning('PTR record was not removed because either key, value or type was not specified');
return;
}
$key = $_GET['key'];
$type = $_GET['type'];
$value = $_GET['value'];
if ($type != 'A')
{
Warning('Requested PTR record was not deleted: PTR record can be only deleted when you are changing A record, you deleted ' . $type . ' record instead');
} else
{
DNS_DeletePTRForARecord($value, $key, '');
}
}
}
public static function GetContents($fc)
{
global $g_auth_session_name, $g_domains, $g_selected_domain, $g_show_hidden_types, $g_hidden_types_present;
global $g_auth_session_name, $g_domains, $g_selected_domain, $g_total_records_count, $g_hidden_records_count, $g_show_hidden_types, $g_hidden_types_present;
// Check toggle for hidden
if (isset($_GET['hidden_types']))
@ -46,12 +82,20 @@ class TabManage
reset($g_domains);
$g_selected_domain = key($g_domains);
}
// First get the record list - this function will fill up g_hidden_types_present variable as well as global counters
$record_list = GetRecordListTable(NULL, $g_selected_domain);
$record_count = "";
if ($g_total_records_count > 0)
{
if ($g_hidden_records_count == 0)
$record_count = " ($g_total_records_count records)";
else
$record_count = " ($g_total_records_count records, $g_hidden_records_count hidden)";
}
$fc->AppendObject(GetSwitcher($fc));
$fc->AppendHeader($g_selected_domain, 2);
$fc->AppendHeader($g_selected_domain . $record_count, 2);
$fc->AppendHtml('<div class="export_csv"><a href="?action=csv&domain=' . $g_selected_domain . '">Export as CSV</a></div>');
$fc->AppendObject(GetStatusOfZoneAsNote($g_selected_domain));
// First get the record list - this function will fill up g_hidden_types_present variable
$record_list = GetRecordListTable(NULL, $g_selected_domain);
if ($g_hidden_types_present === true)
{
// This zone contains some hidden record types, show toggle for user

View file

@ -45,6 +45,10 @@ class TabOverview
$is_ok = false;
$status .= '<span class="glyphicon glyphicon-alert" title="' . $domain_info['maintenance_note'] . '"></span>&nbsp;';
}
if (array_key_exists('note', $domain_info))
{
$status .= '&nbsp;<span class="glyphicon glyphicon-comment" title="' . $domain_info['note'] . '"></span>&nbsp;';
}
if ($is_ok)
return '<span class="glyphicon glyphicon-ok" title="OK"></span>' . $status;

View file

@ -16,6 +16,7 @@ if (!defined('G_DNSTOOL_ENTRY_POINT'))
function IsValidHostName($fqdn)
{
global $g_strict_hostname_checks;
// Few extra checks to prevent shell escaping
if (!ShellEscapeCheck($fqdn))
return false;
@ -29,11 +30,22 @@ function IsValidHostName($fqdn)
return false;
if (psf_string_contains($fqdn, "\n"))
return false;
// security fix + and - are switches used by dig so we need to make sure they aren't first symbol even if strict checking is not enabled
if (psf_string_startsWith($fqdn, "+"))
return false;
if (psf_string_startsWith($fqdn, "-"))
return false;
if ($g_strict_hostname_checks && preg_match('/[^0-9\*a-zA-Z_\-\.]/', $fqdn))
return false;
return true;
}
function SanitizeHostname($hostname)
{
// Right now we do only trim, but maybe in future we will do more, so let's keep it in here
return trim($hostname);
}
function NSupdateEscapeCheck($string)
{
if (psf_string_contains($string, "\n"))

View file

@ -30,6 +30,9 @@ class Zones
if (isset($properties['in_transfer']))
$result[$domain]['in_transfer'] = $properties['in_transfer'];
if (isset($properties['note']))
$result[$domain]['note'] = $properties['note'];
if (isset($properties['maintenance_note']))
$result[$domain]['maintenance_note'] = $properties['maintenance_note'];
@ -78,5 +81,23 @@ class Zones
}
return false;
}
public static function GetDefaultTTL($domain)
{
global $g_default_ttl, $g_domains;
if ($domain === NULL)
return $g_default_ttl;
if (!array_key_exists($domain, $g_domains))
die("No such zone: $domain");
$domain_info = $g_domains[$domain];
if (array_key_exists('ttl', $domain_info))
return $domain_info['ttl'];
return $g_default_ttl;
}
}

View file

@ -42,6 +42,9 @@ if ($g_use_local_bootstrap)
// Start up the program, initialize all sorts of resources, syslog, session data etc.
Initialize();
if ($g_user_config_prefix !== null)
include($g_user_config_prefix.GetCurrentUserName().".php");
// Save us some coding
$psf_containers_auto_insert_child = true;
@ -112,7 +115,10 @@ if (RequireLogin())
$fc->AppendObject(GetLogin());
} else
{
$fc->AppendHeader(G_HEADER);
$header = new DivContainer($fc);
$header->ClassName = 'header';
$header->AppendObject(new Image("favicon.png", "DNS"));
$header->AppendHeader(G_HEADER);
if ($g_logged_in)
$fc->AppendHtml(GetLoginInfo());
@ -128,7 +134,7 @@ if (RequireLogin())
$fc->AppendObject(TabOverview::GetSelectForm($fc));
} else if ($g_action == "manage")
{
ProcessDelete($fc);
TabManage::ProcessDelete($fc);
TabManage::GetContents($fc);
} else if ($g_action == 'csv')
{
@ -144,11 +150,13 @@ if (RequireLogin())
// Process previous inserting call (via submit) in case there was some
TabEdit::Process($fc);
$fc->AppendObject(TabEdit::GetInsertForm($fc));
$fc->AppendObject(TabEdit::GetHelp());
} else if ($g_action == 'edit')
{
// Process previous edit call (via submit) in case there was some
TabEdit::Process($fc);
$fc->AppendObject(TabEdit::GetEditForm($fc));
$fc->AppendObject(TabEdit::GetHelp());
} else if ($g_action == 'batch')
{
// Process any previous pending batch operation
@ -160,8 +168,8 @@ if (RequireLogin())
// Bug workaround - the footer seems to take up some space
$website->AppendHtml("<br><br><br>");
$website->AppendHtmlLine("<footer class='footer'><div class='container'>Created by Petr Bena [petr@bena.rocks] (c) 2018 - 2020, source code at ".
"<a href='http://github.com/benapetr/dnsphpadmin'>http://github.com/benapetr/dnsphpadmin</a> Version: " . G_DNSTOOL_VERSION . "</div></footer>");
$website->AppendHtmlLine("<footer class='footer'><div class='container'>Created by Petr Bena [petr@bena.rocks] (c) 2018 - 2023, source code at ".
"<a href='https://github.com/benapetr/dnsphpadmin'>https://github.com/benapetr/dnsphpadmin</a> Version: " . G_DNSTOOL_VERSION . "</div></footer>");
$website->PrintHtml();

2
psf

@ -1 +1 @@
Subproject commit 1f8fde4565a25e5605736e37631de5be572dddd3
Subproject commit 776af33fed083797593e1da7bb5ec902f7758322

View file

@ -7,6 +7,16 @@ body {
margin-bottom: 60px; /* Margin bottom by footer height */
}
.header img {
float: left;
width: 32px;
}
.header h1 {
position: relative;
left: 10px;
}
.footer {
position: absolute;
bottom: 0;

View file

@ -1,6 +1,12 @@
#!/bin/sh
version=`git tag | tail -1`
#version=`git tag | tail -1`
version=`git for-each-ref --sort=creatordate --format '%(refname) %(creatordate)' refs/tags | tail -1 | sed 's/ .*//' | sed 's/..........//'`
if [ ! -f 'index.php' ]; then
echo "You have to run this from root folder of project"
exit 1
fi
if [ -f "/tmp/dnsphpadmin_$version.tar.gz" ];then
echo "/tmp/dnsphpadmin_$version.tar.gz already exist"
@ -17,6 +23,7 @@ rm -rf "/tmp/dnsphpadmin_$version/.git"
rm -f "/tmp/dnsphpadmin_$version/.gitignore"
rm -f "/tmp/dnsphpadmin_$version/.gitmodules"
rm -rf "/tmp/dnsphpadmin_$version/util"
rm -rf "/tmp/dnsphpadmin_$version/examples"
rm -f "/tmp/dnsphpadmin_$version/.travis.yml"
cd /tmp || exit 1

View file

@ -38,11 +38,15 @@ function CheckZone($data)
$ut = new UnitTest();
$g_default_ttl = 3600;
$ut->Evaluate('Check for non-existence of PTR zones (none) in empty list', Zones::HasPTRZones() === false);
$g_domains['168.192.in-addr.arpa'] = [ ];
$g_domains['192.in-addr.arpa'] = [ ];
$g_domains['192.in-addr.arpa'] = [ 'ttl' => 200 ];
$ut->Evaluate('Check for non-existence of PTR zones (none) in empty list', Zones::HasPTRZones() === true);
$ut->Evaluate('Get zone for FQDN', Zones::GetZoneForFQDN('0.0.168.192.in-addr.arpa') == '168.192.in-addr.arpa');
$ut->Evaluate('Test GetDefaultTTL()', Zones::GetDefaultTTL('168.192.in-addr.arpa') == 3600);
$ut->Evaluate('Test GetDefaultTTL()', Zones::GetDefaultTTL('192.in-addr.arpa') == 200);
$dz1 = raw_zone_to_array(file_get_contents(dirname(__FILE__) . '/testdata/valid.zone1'));
$dz2 = raw_zone_to_array(file_get_contents(dirname(__FILE__) . '/testdata/invalid.zone'));
@ -58,6 +62,9 @@ $ut->Evaluate('Parser test - zone 2', CheckZone($dz3));
$ut->Evaluate('Validator - valid #1', IsValidHostName('insw.cz') === true);
$ut->Evaluate('Validator - valid #2', IsValidHostName('te-st1.petr.bena.rocks') === true);
$ut->Evaluate('Validator - valid #3', IsValidHostName('*.petr.bena.rocks') === true);
$ut->Evaluate('Validator - valid #4', IsValidHostName('_spf.petr.bena.rocks') === true);
$ut->Evaluate('Validator - valid #5', IsValidHostName('wqdcsrv331') === true);
$ut->Evaluate('Validator - valid #6', IsValidHostName('2.168.192.in-addr.arpa') === true);
$ut->Evaluate('Validator - invalid #1', IsValidHostName('-invalid') === false);
$ut->Evaluate('Validator - invalid #2', IsValidHostName('---') === false);
$ut->Evaluate('Validator - invalid #3', IsValidHostName('google domain') === false);
@ -66,8 +73,10 @@ $ut->Evaluate('Validator - invalid #5', IsValidHostName("google.com\ntest") ===
$ut->Evaluate('Validator - invalid #6', IsValidHostName("google.com\ttest") === false);
$ut->Evaluate('Validator - invalid #7', IsValidHostName("'google.com") === false);
$ut->Evaluate('Validator - invalid #8', IsValidHostName("\"google.com") === false);
$ut->Evaluate('Validator - invalid #9', IsValidHostName('$test.org') === false);
$ut->Evaluate('Validator - invalid #10', IsValidHostName('/x.test.org') === false);
echo ("\n");
$ut->PrintResults();
$ut->Exit();
$ut->ExitTest();

View file

@ -15,6 +15,8 @@ function api($action)
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $action);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
return json_decode(curl_exec($ch), true);
}
@ -27,11 +29,48 @@ $ut->Evaluate("get_version", $version['version'] == G_DNSTOOL_VERSION);
$logged = api("action=is_logged");
$ut->Evaluate("is_logged", $logged['is_logged']);
$list = api("action=list_zones");
//var_export($list);
$ut->Evaluate("list_zones contains local test", array_key_exists('test.local', $list));
$ut->Evaluate("list_zones contains update server localhost", $list['test.local']['update_server'] == 'localhost');
$ut->Evaluate("list_zones contains transfer server localhost", $list['test.local']['transfer_server'] == 'localhost');
$zone_transfer = api("action=list_records&zone=test.local");
$ut->Evaluate("list_records contains SOA for local", $zone_transfer[0][3] == "SOA");
//var_export($zone_transfer);
$output = api('action=create_record&record=test1.test.local&ttl=10&type=A&value=10.2.2.8');
$ut->Evaluate('create_record test A', $output['result'] == 'success');
$record = api('action=get_record&record=test1.test.local');
$ut->Evaluate('get_record TTL', $record[0][1] == '10');
$ut->Evaluate('get_record value', $record[0][4] == '10.2.2.8');
$ut->Evaluate('get_record name', $record[0][0] == 'test1.test.local.');
$output = api('action=delete_record&record=test1.test.local&ttl=10&type=A&value=10.2.2.8');
$ut->Evaluate('delete_record test A', $output['result'] == 'success');
$output = api('action=delete_record&record=test.local&ttl=10&type=SOA&value=');
$ut->Evaluate("delete_record that isn't allowed", isset($output['error']));
$output = api('action=create_record&record=-.test.local&ttl=10&type=A&value=10.2.2.8');
$ut->Evaluate('create invalid record #1', isset($output['error']));
$output = api('action=create_record&record=$.test.local&ttl=10&type=A&value=10.2.2.8');
$ut->Evaluate('create invalid record #2', isset($output['error']));
$fqdn = api('action=get_zone_for_fqdn&fqdn=meep.test.local');
$ut->Evaluate('get zone for FQDN (test.local name)', $fqdn['zone'] == 'test.local');
$fqdn = api('action=get_zone_for_fqdn&fqdn=local');
$ut->Evaluate('get zone for nonexistent FQDN', $fqdn['error'] != '');
$login = api("action=logout");
$ut->Evaluate("logout", $login['result'] == 'success');
echo ("\n\n\n");
$ut->PrintResults();
$ut->Exit();
$ut->ExitTest();