├── .gitignore ├── LICENSE ├── NOTICE ├── Pest.php ├── PestJSON.php ├── README.md ├── bindzonefile.php ├── config └── config-sample.ini ├── core.php ├── email.php ├── extensions └── README ├── ldap.php ├── migrations ├── 001.php ├── 002.php ├── 003.php ├── 004.php ├── 005.php └── 006.php ├── model ├── change.php ├── changeset.php ├── comment.php ├── dbdirectory.php ├── migrationdirectory.php ├── nstemplate.php ├── pendingupdate.php ├── record.php ├── replicationtype.php ├── replicationtypedirectory.php ├── resourcerecord.php ├── resourcerecordset.php ├── soatemplate.php ├── template.php ├── templatedirectory.php ├── user.php ├── useralert.php ├── userdirectory.php ├── zone.php ├── zoneaccess.php └── zonedirectory.php ├── pagesection.php ├── phpunit.xml ├── powerdns.php ├── public_html ├── book_next.png ├── bootstrap │ ├── css │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.css.map │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap-theme.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ └── npm.js ├── extra.js ├── init.php ├── ipaddr │ ├── LICENSE │ ├── SOURCE │ └── ipaddr.min.js ├── jquery │ └── jquery-3.7.1.min.js ├── logo-header-opera.png ├── screenshot-changelog.png ├── screenshot-zoneedit.png └── style.css ├── requesthandler.php ├── router.php ├── routes.php ├── scripts ├── create_admin_account.php ├── full_git_tracked_export.php └── ldap_update.php ├── templates ├── apihelp.php ├── base.php ├── csrf.php ├── error403.php ├── error404.php ├── error500.php ├── error503.php ├── error503_upstream.php ├── functions.php ├── settings.php ├── template.php ├── templates.php ├── user.php ├── user_not_found.php ├── users.php ├── zone.php ├── zone_add_failed.php ├── zone_update_failed.php ├── zonedeleted.php ├── zoneexport.php ├── zoneimport.php ├── zones.php ├── zonesplit.php └── zonesplitcompleted.php ├── tests ├── DNSContentTest.php ├── DNSKEYTest.php ├── DNSNameTest.php └── DNSTimeTest.php ├── vagrant ├── README.md ├── Vagrantfile └── ansible │ ├── config.sample.yml │ ├── dns-ui.yml │ └── roles │ └── dns-ui │ ├── defaults │ └── main.yml │ ├── tasks │ ├── apache2.yml │ ├── config.yml │ ├── dns-ui.yml │ ├── ldap.yml │ ├── main.yml │ ├── packages.yml │ ├── ping.yml │ ├── postgresql.yml │ ├── powerdns.yml │ └── repo.yml │ ├── templates │ ├── apache2.conf.j2 │ └── pdns.gpgsql.conf.j2 │ └── vars │ └── main.yml └── views ├── api.php ├── csrf.php ├── error403.php ├── error404.php ├── error500.php ├── error503.php ├── error503_upstream.php ├── home.php ├── settings.php ├── template.php ├── templates.php ├── user.php ├── users.php ├── zone.php ├── zoneexport.php ├── zoneimport.php ├── zones.php └── zonesplit.php /.gitignore: -------------------------------------------------------------------------------- 1 | config/config.ini 2 | public_html/site.css 3 | extensions/*.php 4 | vagrant/*.retry 5 | vagrant/*.log 6 | vagrant/.vagrant 7 | vagrant/ansible/config.yml 8 | vagrant/ansible/*.retry 9 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | NOTICE 2 | 3 | Copyright 2013-2018 Opera Software AS 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | 18 | THIRD PARTY ACKNOWLEDGEMENTS 19 | 20 | Component: Pest/JSON 21 | 22 | Copyright (C) 2011 by University of Toronto 23 | 24 | Permission is hereby granted, free of charge, to any person obtaining a copy 25 | of this software and associated documentation files (the "Software"), to deal 26 | in the Software without restriction, including without limitation the rights 27 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 28 | copies of the Software, and to permit persons to whom the Software is 29 | furnished to do so, subject to the following conditions: 30 | 31 | The above copyright notice and this permission notice shall be included in all 32 | copies or substantial portions of the Software. 33 | 34 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 36 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 37 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 38 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 39 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 40 | 41 | 42 | 43 | Component: stickyHeader 44 | 45 | Do whatever you want with it just give me, Russell Heimlich, credit. 46 | 47 | Creative Commons Attribution 3.0 Unported http://creativecommons.org/licenses/by/3.0/ 48 | 49 | 50 | 51 | Component: Bootstrap Framework 52 | 53 | The MIT License (MIT) 54 | 55 | Copyright (c) 2011-2017 Twitter, Inc. 56 | Copyright (c) 2011-2017 The Bootstrap Authors 57 | 58 | Permission is hereby granted, free of charge, to any person obtaining a copy 59 | of this software and associated documentation files (the "Software"), to deal 60 | in the Software without restriction, including without limitation the rights 61 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 62 | copies of the Software, and to permit persons to whom the Software is 63 | furnished to do so, subject to the following conditions: 64 | 65 | The above copyright notice and this permission notice shall be included in 66 | all copies or substantial portions of the Software. 67 | 68 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 69 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 70 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 71 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 72 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 73 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 74 | THE SOFTWARE. 75 | 76 | 77 | 78 | Component: jQuery JavaScript Library 79 | 80 | Copyright JS Foundation and other contributors, https://js.foundation/ 81 | 82 | This software consists of voluntary contributions made by many 83 | individuals. For exact contribution history, see the revision history 84 | available at https://github.com/jquery/jquery 85 | 86 | The following license applies to all parts of this software except as 87 | documented below: 88 | 89 | ==== 90 | 91 | Permission is hereby granted, free of charge, to any person obtaining 92 | a copy of this software and associated documentation files (the 93 | "Software"), to deal in the Software without restriction, including 94 | without limitation the rights to use, copy, modify, merge, publish, 95 | distribute, sublicense, and/or sell copies of the Software, and to 96 | permit persons to whom the Software is furnished to do so, subject to 97 | the following conditions: 98 | 99 | The above copyright notice and this permission notice shall be 100 | included in all copies or substantial portions of the Software. 101 | 102 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 103 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 104 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 105 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 106 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 107 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 108 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 109 | 110 | ==== 111 | 112 | All files located in the node_modules and external directories are 113 | externally maintained libraries used by this software which have their 114 | own licenses; we recommend you read them, as their terms may differ from 115 | the terms above. -------------------------------------------------------------------------------- /PestJSON.php: -------------------------------------------------------------------------------- 1 | = 400 status codes, an exception is thrown with $e->getMessage() 12 | * containing the error message that the server produced. User code will have to 13 | * json_decode() that manually, if applicable, because the PHP Exception base 14 | * class does not accept arrays for the exception message and some JSON/REST servers 15 | * do not produce nice JSON 16 | * 17 | * If you don't want to have exceptions thrown when there are errors encoding or 18 | * decoding JSON set the `throwEncodingExceptions` property to FALSE. 19 | * 20 | * See http://github.com/educoder/pest for details. 21 | * 22 | * This code is licensed for use, modification, and distribution 23 | * under the terms of the MIT License (see http://en.wikipedia.org/wiki/MIT_License) 24 | */ 25 | 26 | require_once 'Pest.php'; 27 | 28 | class PestJSON extends Pest 29 | { 30 | const JSON_ERROR_UNKNOWN = 1000; 31 | 32 | /** 33 | * @var bool Throw exceptions on JSON encoding errors? 34 | */ 35 | public $throwJsonExceptions = true; 36 | 37 | /** 38 | * Perform an HTTP POST 39 | * 40 | * @param string $url 41 | * @param array $data 42 | * @param array $headers 43 | * @return string 44 | */ 45 | public function post($url, $data, $headers = array()) 46 | { 47 | return parent::post($url, $this->jsonEncode($data), $headers); 48 | } 49 | 50 | /** 51 | * Perform HTTP PUT 52 | * 53 | * @param string $url 54 | * @param array $data 55 | * @param array $headers 56 | * @return string 57 | */ 58 | public function put($url, $data, $headers = array()) 59 | { 60 | return parent::put($url, $this->jsonEncode($data), $headers); 61 | } 62 | 63 | /** 64 | * Perform an HTTP PATCH 65 | * 66 | * @param string $url 67 | * @param array $data 68 | * @param array $headers 69 | * @return string 70 | */ 71 | public function patch($url, $data, $headers = array()) 72 | { 73 | return parent::patch($url, $this->jsonEncode($data), $headers); 74 | } 75 | 76 | /** 77 | * JSON encode with error checking 78 | * 79 | * @param mixed $data 80 | * @return string 81 | * @throws Pest_Json_Encode 82 | */ 83 | public function jsonEncode($data) 84 | { 85 | $ret = json_encode($data); 86 | 87 | if ($ret === false && $this->throwJsonExceptions) { 88 | throw new Pest_Json_Encode( 89 | 'Encoding error: ' . $this->getLastJsonErrorMessage(), 90 | $this->getLastJsonErrorCode() 91 | ); 92 | } 93 | 94 | return $ret; 95 | } 96 | 97 | /** 98 | * Decode a JSON string with error checking 99 | * 100 | * @param string $data 101 | * @param bool $asArray 102 | * @throws Pest_Json_Decode 103 | * @return mixed 104 | */ 105 | public function jsonDecode($data, $asArray=false) 106 | { 107 | $ret = json_decode($data, $asArray); 108 | 109 | if ($ret === null && $this->hasJsonDecodeFailed() && $this->throwJsonExceptions) { 110 | throw new Pest_Json_Decode( 111 | 'Decoding error: ' . $this->getLastJsonErrorMessage(), 112 | $this->getLastJsonErrorCode() 113 | ); 114 | } 115 | 116 | return $ret; 117 | } 118 | 119 | /** 120 | * Get last JSON error message 121 | * 122 | * @return string 123 | */ 124 | public function getLastJsonErrorMessage() 125 | { 126 | // For PHP < 5.3, just return "Unknown" 127 | if (!function_exists('json_last_error')) { 128 | return "Unknown"; 129 | } 130 | 131 | // Use the newer JSON error message function if it exists 132 | if (function_exists('json_last_error_msg')) { 133 | return(json_last_error_msg()); 134 | } 135 | 136 | $lastError = json_last_error(); 137 | 138 | // PHP 5.3+ only 139 | if (defined('JSON_ERROR_UTF8') && $lastError === JSON_ERROR_UTF8) { 140 | return 'Malformed UTF-8 characters, possibly incorrectly encoded'; 141 | } 142 | 143 | switch ($lastError) { 144 | case JSON_ERROR_DEPTH: 145 | return 'Maximum stack depth exceeded'; 146 | break; 147 | case JSON_ERROR_STATE_MISMATCH: 148 | return 'Underflow or the modes mismatch'; 149 | break; 150 | case JSON_ERROR_CTRL_CHAR: 151 | return 'Unexpected control character found'; 152 | break; 153 | case JSON_ERROR_SYNTAX: 154 | return 'Syntax error, malformed JSON'; 155 | break; 156 | default: 157 | return 'Unknown'; 158 | break; 159 | } 160 | } 161 | 162 | 163 | /** 164 | * Get last JSON error code 165 | * @return int|null 166 | */ 167 | public function getLastJsonErrorCode() 168 | { 169 | // For PHP < 5.3, just return the PEST code for unknown errors 170 | if (!function_exists('json_last_error')) { 171 | return self::JSON_ERROR_UNKNOWN; 172 | } 173 | 174 | return json_last_error(); 175 | } 176 | 177 | /** 178 | * Check if decoding failed 179 | * @return bool 180 | */ 181 | private function hasJsonDecodeFailed() 182 | { 183 | // you cannot safely determine decode errors in PHP < 5.3 184 | if (!function_exists('json_last_error')) { 185 | return false; 186 | } 187 | 188 | return json_last_error() !== JSON_ERROR_NONE; 189 | } 190 | 191 | /** 192 | * Process body 193 | * @param string $body 194 | * @return mixed|string 195 | */ 196 | public function processBody($body) 197 | { 198 | if($body == '') return false; 199 | return $this->jsonDecode($body); 200 | } 201 | 202 | /** 203 | * Prepare request 204 | * 205 | * @param array $opts 206 | * @param string $url 207 | * @return resource 208 | */ 209 | protected function prepRequest($opts, $url) 210 | { 211 | $opts[CURLOPT_HTTPHEADER][] = 'Accept: application/json'; 212 | $opts[CURLOPT_HTTPHEADER][] = 'Content-Type: application/json'; 213 | return parent::prepRequest($opts, $url); 214 | } 215 | } 216 | 217 | // JSON Errors 218 | /* decode */ 219 | class Pest_Json_Decode extends Pest_ClientError 220 | {} 221 | 222 | /* encode */ 223 | class Pest_Json_Encode extends Pest_ClientError 224 | {} 225 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Opera DNS UI 2 | ============ 3 | 4 | A tool to manage a PowerDNS authoritative server in a corporate LDAP-driven environment. 5 | 6 | Features 7 | -------- 8 | 9 | * Connects to PowerDNS via its JSON API. 10 | * Allows login managed by LDAP server. 11 | * Create zones; add, edit and delete records. 12 | * Grant multiple users access to administer a zone. 13 | * Lower access level that allows to view a zone and *request* changes. 14 | * Provides its own JSON API for making changes to DNS records. 15 | * Keeps a changelog of all DNS changes done through it. 16 | * (Optionally) export all zones as bind-format zone files and store changes in git. 17 | 18 | Demo 19 | ---- 20 | 21 | You can view the DNS UI in action on the [demonstration server](https://dnsui.xiven.com/). 22 | 23 | Use one of the following sets of username / password credentials to log in: 24 | 25 | * testuser / testuser - normal user with admin access granted to a few domains 26 | * testadmin / testadmin - admin user 27 | 28 | All data on this demonstration server is reset nightly at 00:00 UTC. 29 | 30 | Compatibility 31 | ------------- 32 | 33 | The current version is only compatible with PowerDNS 4.0.4 and higher. Previous 4.0.x versions suffer from a critical API bug related to TTL values. 34 | 35 | As another option, you can use PowerDNS 3 with 36 | [Opera DNS UI v0.1.3](https://github.com/operasoftware/dns-ui/releases/tag/v0.1.3), but the 0.1 version of the DNS UI will not receive any new features or non-critical fixes. 37 | 38 | Requirements 39 | ------------ 40 | 41 | * Apache 2.2.18+ / nginx 42 | * PHP 7.0+ 43 | * PHP intl (Internationalization Functions) extension 44 | * PHP JSON extension 45 | * PHP CURL extension 46 | * PHP Multibyte String extension 47 | * PHP LDAP extension 48 | * PHP PDO_PGSQL extension 49 | * PostgreSQL database 50 | * PowerDNS authoritative server 4.0.4+ 51 | 52 | Installation 53 | ------------ 54 | 55 | 1. Configure PowerDNS: 56 | 57 | webserver=yes 58 | webserver-address=... 59 | webserver-allow-from=... 60 | webserver-port=... 61 | api=yes 62 | api-key=... 63 | 64 | 2. Clone this repo to somewhere *outside* of your default web server document root. 65 | 66 | 3. Create a postgresql user and database. 67 | 68 | createuser -P dnsui-user 69 | createdb -O dnsui-user dnsui-db 70 | 71 | 4. Add the following directives to your web server configuration (eg. virtual host config): 72 | 73 | * Apache: 74 | 75 | DocumentRoot /path/to/dnsui/public_html 76 | DirectoryIndex init.php 77 | FallbackResource /init.php 78 | AllowEncodedSlashes NoDecode 79 | 80 | [Full Apache virtualhost example](https://github.com/operasoftware/dns-ui/wiki/Example-configuration:-apache) 81 | 82 | * nginx: 83 | 84 | root /path/to/dnsui/public_html; 85 | index init.php; 86 | location / { 87 | try_files $uri $uri/ @php; 88 | } 89 | location @php { 90 | rewrite ^/(.*)$ /init.php/$1 last; 91 | } 92 | location /init.php { 93 | fastcgi_pass unix:/run/php/php7.0-fpm.sock ; 94 | include /etc/nginx/snippets/fastcgi-php.conf; 95 | } 96 | 97 | [Full nginx server example](https://github.com/operasoftware/dns-ui/wiki/Example-configuration:-nginx) 98 | 99 | 5. Set up an authentication module for your virtual host (eg. authnz_ldap for Apache). 100 | 101 | 6. Copy the file `config/config-sample.ini` to `config/config.ini` and edit the settings as required. 102 | 103 | 7. Set `scripts/ldap_update.php` to run on a regular cron job. 104 | 105 | Usage 106 | ----- 107 | 108 | Anyone in the LDAP group defined under `admin_group_cn` in `config/config.ini` will be able to add and modify all zones. 109 | They will also be able to grant access under "User access" for any zone to any number of users. 110 | 111 | API 112 | --- 113 | 114 | By going to the URL `/api/v2` with your web browser you can see documentation of the rest API, including all of the 115 | available API methods. [See this on the demo server](https://dnsui.xiven.com/api/v2). 116 | 117 | Screenshots 118 | ----------- 119 | 120 | ### Editing multiple records in one batch 121 | ![Editing multiple records in one batch](public_html/screenshot-zoneedit.png) 122 | 123 | ### Comprehensive changelog of all changes 124 | ![Comprehensive changelog of all changes](public_html/screenshot-changelog.png) 125 | 126 | License 127 | ------- 128 | 129 | Copyright 2013-2018 Opera Software 130 | 131 | Licensed under the Apache License, Version 2.0 (the "License"); 132 | you may not use this file except in compliance with the License. 133 | You may obtain a copy of the License at 134 | 135 | http://www.apache.org/licenses/LICENSE-2.0 136 | 137 | Unless required by applicable law or agreed to in writing, software 138 | distributed under the License is distributed on an "AS IS" BASIS, 139 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 140 | See the License for the specific language governing permissions and 141 | limitations under the License. 142 | -------------------------------------------------------------------------------- /config/config-sample.ini: -------------------------------------------------------------------------------- 1 | ; DNS UI config file 2 | [web] 3 | enabled = 1 4 | ; Do not include a trailing / in the baseurl 5 | baseurl = https://dns.example.com 6 | logo = /logo-header-opera.png 7 | ; header and footer may contain HTML. Literal & " < and > should be escaped as & " < $gt; 8 | header = 'DNS management' 9 | footer = 'Developed by Opera Software.' 10 | ; Enable this option if you want system and zone admins to be forced to request changes just like the operators. 11 | ;force_change_review = 1 12 | ; Enable this option if you want all users to be forced to enter a comment for every change made. 13 | ;force_change_comment = 1 14 | 15 | [email] 16 | enabled = 1 17 | ; The mail address that outgoing mails will be sent from 18 | from_address = dns@example.com 19 | from_name = "DNS management system" 20 | ; Where to mail problem notifications to 21 | report_address = admin@example.com 22 | report_name = "Domain administrator" 23 | ; You can use the reroute directive to redirect all outgoing mail to a single 24 | ; mail address - typically for temporary testing purposes 25 | ;reroute = example@example.com 26 | 27 | [database] 28 | ; Connection details to the Postgres database 29 | dsn = "pgsql:host=localhost dbname=dnsui" 30 | username = username 31 | password = password 32 | 33 | [authentication] 34 | ; compare the user ID's case? (on by default) 35 | user_case_sensitive = 1 36 | 37 | [php_auth] 38 | enabled = 0 39 | admin_group = "systems" 40 | 41 | [ldap] 42 | enabled = 1 43 | ; Address to connect to LDAP server 44 | host = ldaps://ldap.example.com:636 45 | ; Use StartTLS for connection security (recommended if using ldap:// instead of ldaps:// above) 46 | starttls = 0 47 | ; LDAP subtree containing USER entries 48 | dn_user = "ou=users,dc=example,dc=com" 49 | ; LDAP subtree containing GROUP entries 50 | dn_group = "ou=groups,dc=example,dc=com" 51 | ; Set to 1 if the LDAP library should process referrals. In most cases this 52 | ; is not needed, and for AD servers it can cause errors when querying the 53 | ; whole tree. 54 | follow_referrals = 0 55 | 56 | ; Leave bind_dn empty if binding is not required 57 | bind_dn = 58 | bind_password = 59 | 60 | ; User attributes 61 | user_id = uid 62 | user_name = cn 63 | user_email = mail 64 | 65 | ; If inactive users exist in your LDAP directory, filter with the following settings: 66 | ; Field to filter on: 67 | ;user_active = organizationalstatus 68 | ; Use *one* of user_active_true or user_active_false 69 | ; user_active_true means user is active if the user_active field equals its value 70 | ;user_active_true = 'current' 71 | ; user_active_false means user is active if the user_active field does not equal its value 72 | ;user_active_false = 'former' 73 | 74 | ; Group membership attributes. Examples below are for typical setups: 75 | ; 76 | ; POSIX groups 77 | ; group_member = memberUid 78 | ; group_member_value = uid 79 | ; 80 | ; Group-of-names groups 81 | ; group_member = member 82 | ; group_member_value = dn 83 | ; 84 | ; Active Directory LDAP_MATCHING_RULE_IN_CHAIN (nested groups) 85 | ; group_member = "member:1.2.840.113556.1.4.1941:" 86 | ; group_member_value = "dn" 87 | ; 88 | ; Attribute of group where members are stored 89 | group_member = memberUid 90 | ; User attribute to compare with 91 | group_member_value = uid 92 | 93 | ; Members of admin_group are given full access to DNS UI web interface 94 | admin_group_cn = administrators 95 | 96 | [powerdns] 97 | api_url = "http://localhost:8081/api/v1/servers/localhost" 98 | api_key = api_key 99 | 100 | ; Earlier version of DNS UI use INCEPTION-INCREMENT as the default for soa_edit_api but this has 101 | ; been dropped in PowerDNS 4.2; DEFAULT should be used instead, but one can set a different value 102 | ; for backwards compatibility with earlier PowerDNS versions. 103 | soa_edit_api = DEFAULT 104 | 105 | [dns] 106 | ; Enable DNSSEC view UI (requires PowerDNS 4.1) 107 | dnssec = 0 108 | ; Allow enabling/disabling DNSSEC through the UI 109 | dnssec_edit = 1 110 | 111 | ; If enabled (the default), matching PTR records will be automatically created 112 | ; when new A or AAAA records are added. 113 | autocreate_reverse_records = 1 114 | 115 | ; Space-separated lists 116 | local_zone_suffixes = "localdomain" 117 | local_ipv4_ranges = "10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 127.0.0.0/8" 118 | local_ipv6_ranges = "fd00::/8 ::1/128" 119 | 120 | ; if non-empty, only allow entering these (comma seperated) values in the Classification column 121 | ; Note that this is purely a front-end restriction, designed to avoid accidental errors. 122 | ;classification_whitelist = "internal,public" 123 | 124 | [git_tracked_export] 125 | ; If enabled, will export zones as bind9 zone format to the specified path and 126 | ; will git add / git commit on behalf of the active user. 127 | ; path must be a git repository writable by the webserver user. 128 | enabled = 0 129 | path = /tmp/dns-export 130 | -------------------------------------------------------------------------------- /extensions/README: -------------------------------------------------------------------------------- 1 | For extending or replacing functionality. 2 | All .php files in this directory are automatically include()'d. -------------------------------------------------------------------------------- /ldap.php: -------------------------------------------------------------------------------- 1 | conn = null; 28 | $this->host = $host; 29 | $this->starttls = $starttls; 30 | $this->bind_dn = $bind_dn; 31 | $this->bind_password = $bind_password; 32 | $this->options = $options; 33 | } 34 | 35 | private function connect() { 36 | $this->conn = ldap_connect($this->host); 37 | if($this->conn === false) throw new LDAPConnectionFailureException('Invalid LDAP connection settings'); 38 | if($this->starttls) { 39 | if(!ldap_start_tls($this->conn)) throw new LDAPConnectionFailureException('Could not initiate TLS connection to LDAP server'); 40 | } 41 | foreach($this->options as $option => $value) { 42 | ldap_set_option($this->conn, $option, $value); 43 | } 44 | if(!empty($this->bind_dn)) { 45 | if(!ldap_bind($this->conn, $this->bind_dn, $this->bind_password)) throw new LDAPConnectionFailureException('Could not bind to LDAP server'); 46 | } 47 | } 48 | 49 | public function search($basedn, $filter, $fields = array(), $sort = array()) { 50 | if(is_null($this->conn)) $this->connect(); 51 | if(empty($fields)) $r = @ldap_search($this->conn, $basedn, $filter); 52 | else $r = @ldap_search($this->conn, $basedn, $filter, $fields); 53 | $sort = array_reverse($sort); 54 | foreach($sort as $field) { 55 | @ldap_sort($this->conn, $r, $field); 56 | } 57 | if($r) { 58 | // Fetch entries 59 | $result = @ldap_get_entries($this->conn, $r); 60 | unset($result['count']); 61 | $items = array(); 62 | foreach($result as $item) { 63 | unset($item['count']); 64 | $itemResult = array(); 65 | foreach($item as $key => $values) { 66 | if(!is_int($key)) { 67 | if(is_array($values)) { 68 | unset($values['count']); 69 | if(count($values) == 1) $values = $values[0]; 70 | } 71 | $itemResult[$key] = $values; 72 | } 73 | } 74 | $items[] = $itemResult; 75 | } 76 | return $items; 77 | } 78 | return false; 79 | } 80 | 81 | public static function escape($str = '') { 82 | $metaChars = array("\\00", "\\", "(", ")", "*"); 83 | $quotedMetaChars = array(); 84 | foreach($metaChars as $key => $value) { 85 | $quotedMetaChars[$key] = '\\'. dechex(ord($value)); 86 | } 87 | $str = str_replace($metaChars, $quotedMetaChars, $str); 88 | return $str; 89 | } 90 | } 91 | 92 | class LDAPConnectionFailureException extends RuntimeException {} 93 | -------------------------------------------------------------------------------- /migrations/001.php: -------------------------------------------------------------------------------- 1 | database->exec(" 5 | CREATE TABLE migration ( 6 | id integer NOT NULL, 7 | name text NOT NULL, 8 | applied timestamp without time zone NOT NULL, 9 | CONSTRAINT migration_pkey PRIMARY KEY (id) 10 | ) WITH (OIDS=FALSE) 11 | "); 12 | -------------------------------------------------------------------------------- /migrations/002.php: -------------------------------------------------------------------------------- 1 | database->exec('SELECT * FROM config'); 9 | } catch(PDOException $e) { 10 | $this->database->exec("CREATE TYPE access_level AS ENUM ('administrator', 'operator')"); 11 | $this->database->exec("CREATE TYPE auth_realm AS ENUM ('LDAP', 'local')"); 12 | 13 | $this->database->exec(' 14 | CREATE TABLE change ( 15 | id serial, 16 | changeset_id integer NOT NULL, 17 | before bytea, 18 | after bytea, 19 | CONSTRAINT change_pkey PRIMARY KEY (id) 20 | ) WITH (OIDS=FALSE) 21 | '); 22 | 23 | $this->database->exec(' 24 | CREATE TABLE changeset ( 25 | id serial, 26 | zone_id integer NOT NULL, 27 | author_id integer NOT NULL, 28 | change_date timestamp without time zone NOT NULL, 29 | comment text, 30 | deleted integer DEFAULT 0 NOT NULL, 31 | added integer DEFAULT 0 NOT NULL, 32 | requester_id integer, 33 | CONSTRAINT changeset_pkey PRIMARY KEY (id) 34 | ) WITH (OIDS=FALSE) 35 | '); 36 | 37 | $this->database->exec(' 38 | CREATE TABLE config ( 39 | id integer NOT NULL, 40 | default_soa_template integer, 41 | default_ns_template integer, 42 | CONSTRAINT config_pkey PRIMARY KEY (id) 43 | ) WITH (OIDS=FALSE) 44 | '); 45 | $this->database->exec('INSERT INTO "config" (id) VALUES (1)'); 46 | 47 | $this->database->exec(' 48 | CREATE TABLE ns_template ( 49 | id serial, 50 | name text NOT NULL, 51 | nameservers text NOT NULL, 52 | CONSTRAINT ns_template_pkey PRIMARY KEY (id), 53 | CONSTRAINT ns_template_name_key UNIQUE (name) 54 | ) WITH (OIDS=FALSE) 55 | '); 56 | 57 | $this->database->exec(' 58 | CREATE TABLE pending_update ( 59 | id serial, 60 | zone_id integer NOT NULL, 61 | author_id integer, 62 | request_date timestamp without time zone NOT NULL, 63 | raw_data bytea NOT NULL, 64 | CONSTRAINT pending_change_pkey PRIMARY KEY (id) 65 | ) WITH (OIDS=FALSE) 66 | '); 67 | 68 | $this->database->exec(' 69 | CREATE TABLE soa_template ( 70 | id serial, 71 | name text NOT NULL, 72 | primary_ns text NOT NULL, 73 | contact text NOT NULL, 74 | refresh integer NOT NULL, 75 | retry integer NOT NULL, 76 | expire integer NOT NULL, 77 | default_ttl integer NOT NULL, 78 | soa_ttl integer, 79 | CONSTRAINT soa_template_pkey PRIMARY KEY (id), 80 | CONSTRAINT soa_template_name_key UNIQUE (name) 81 | ) WITH (OIDS=FALSE) 82 | '); 83 | 84 | $this->database->exec(' 85 | CREATE TABLE "user" ( 86 | id serial, 87 | uid text, 88 | name text, 89 | email text, 90 | auth_realm auth_realm, 91 | active integer, 92 | admin integer DEFAULT 0 NOT NULL, 93 | developer integer DEFAULT 0 NOT NULL, 94 | csrf_token text, 95 | CONSTRAINT user_pkey PRIMARY KEY (id), 96 | CONSTRAINT user_uid_key UNIQUE (uid) 97 | ) WITH (OIDS=FALSE) 98 | '); 99 | 100 | $this->database->exec(' 101 | CREATE TABLE user_alert ( 102 | id serial, 103 | user_id integer, 104 | class text, 105 | content text, 106 | escaping integer, 107 | CONSTRAINT user_alert_pkey PRIMARY KEY (id) 108 | ) WITH (OIDS=FALSE) 109 | '); 110 | 111 | $this->database->exec(' 112 | CREATE TABLE zone ( 113 | id serial, 114 | pdns_id text, 115 | name text, 116 | serial bigint, 117 | active boolean DEFAULT true NOT NULL, 118 | account text, 119 | CONSTRAINT zone_pkey PRIMARY KEY (id), 120 | CONSTRAINT zone_pdns_id_key UNIQUE (pdns_id) 121 | ) WITH (OIDS=FALSE) 122 | '); 123 | 124 | $this->database->exec(" 125 | CREATE TABLE zone_access ( 126 | zone_id integer NOT NULL, 127 | user_id integer NOT NULL, 128 | level access_level DEFAULT 'administrator'::access_level NOT NULL, 129 | CONSTRAINT zone_admin_pkey PRIMARY KEY (zone_id, user_id) 130 | ) WITH (OIDS=FALSE) 131 | "); 132 | 133 | $this->database->exec(' 134 | ALTER TABLE ONLY change 135 | ADD CONSTRAINT change_changeset_id_fkey FOREIGN KEY (changeset_id) REFERENCES changeset(id) ON DELETE CASCADE; 136 | '); 137 | 138 | $this->database->exec(' 139 | ALTER TABLE ONLY changeset 140 | ADD CONSTRAINT changeset_author_id_fkey FOREIGN KEY (author_id) REFERENCES "user"(id), 141 | ADD CONSTRAINT changeset_requester_id_fkey FOREIGN KEY (requester_id) REFERENCES "user"(id), 142 | ADD CONSTRAINT changeset_zone_id_fkey FOREIGN KEY (zone_id) REFERENCES zone(id) ON DELETE CASCADE; 143 | '); 144 | 145 | $this->database->exec(' 146 | ALTER TABLE ONLY config 147 | ADD CONSTRAINT config_default_ns_template_fkey FOREIGN KEY (default_ns_template) REFERENCES ns_template(id) ON DELETE SET NULL; 148 | '); 149 | 150 | $this->database->exec(' 151 | ALTER TABLE ONLY pending_update 152 | ADD CONSTRAINT pending_change_zone_id_fkey FOREIGN KEY (zone_id) REFERENCES zone(id) ON DELETE CASCADE; 153 | '); 154 | 155 | $this->database->exec(' 156 | ALTER TABLE ONLY user_alert 157 | ADD CONSTRAINT user_alert_user_id_fkey FOREIGN KEY (user_id) REFERENCES "user"(id); 158 | '); 159 | 160 | $this->database->exec(' 161 | ALTER TABLE ONLY zone_access 162 | ADD CONSTRAINT zone_admin_user_id_fkey FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE, 163 | ADD CONSTRAINT zone_admin_zone_id_fkey FOREIGN KEY (zone_id) REFERENCES zone(id) ON DELETE CASCADE; 164 | '); 165 | } 166 | -------------------------------------------------------------------------------- /migrations/003.php: -------------------------------------------------------------------------------- 1 | database->exec(' 6 | CREATE TABLE replication_type ( 7 | id serial, 8 | name text NOT NULL, 9 | description text, 10 | CONSTRAINT replication_type_pkey PRIMARY KEY (id), 11 | CONSTRAINT replication_type_name_key UNIQUE (name) 12 | ) WITH (OIDS=FALSE) 13 | '); 14 | $this->database->exec("INSERT INTO replication_type VALUES (1, 'Native', 'Native replication means that PowerDNS will not send out DNS update notifications, nor will react to them. PowerDNS assumes that the backend is taking care of replication unaided.')"); 15 | $this->database->exec("INSERT INTO replication_type VALUES (2, 'Master', 'When operating as a master, PowerDNS sends out notifications of changes to slaves, which react to these notifications by querying PowerDNS to see if the zone changed, and transferring its contents if it has.')"); 16 | 17 | // Due to a mistake, the config.default_soa_template field was originally created without a foreign key constraint. 18 | // We add it below but first we need to ensure that the existing data will not violate the constraint. 19 | $stmt = $this->database->prepare(' 20 | SELECT config.default_soa_template, soa_template.id 21 | FROM config 22 | LEFT JOIN soa_template ON soa_template.id = config.default_soa_template 23 | '); 24 | $stmt->execute(); 25 | $row = $stmt->fetch(PDO::FETCH_ASSOC); 26 | if(is_null($row['id']) && !is_null($row['default_soa_template'])) { 27 | // config points to an SOA template that doesn't exist any more - reset to null 28 | $this->database->exec('UPDATE config SET default_soa_template = NULL'); 29 | } 30 | 31 | // Add new default_replication_type field to config table and also add missing constraint on default_soa_template 32 | $this->database->exec(' 33 | ALTER TABLE ONLY config 34 | ADD default_replication_type integer, 35 | ADD CONSTRAINT config_default_replication_type_fkey FOREIGN KEY (default_replication_type) REFERENCES replication_type(id) ON DELETE SET NULL, 36 | ADD CONSTRAINT config_default_soa_template_fkey FOREIGN KEY (default_soa_template) REFERENCES soa_template(id) ON DELETE SET NULL; 37 | '); 38 | // Maintain previous behaviour by setting Master as default replication type 39 | $this->database->exec("UPDATE config SET default_replication_type = 2"); 40 | 41 | // Add 'kind' field to zone table 42 | $this->database->exec(' 43 | ALTER TABLE ONLY zone 44 | ADD kind text; 45 | '); 46 | -------------------------------------------------------------------------------- /migrations/004.php: -------------------------------------------------------------------------------- 1 | database->exec(' 6 | ALTER TABLE ONLY zone 7 | ADD dnssec integer; 8 | '); 9 | -------------------------------------------------------------------------------- /migrations/005.php: -------------------------------------------------------------------------------- 1 | database->exec(' 6 | CREATE TABLE zone_delete ( 7 | zone_id integer NOT NULL, 8 | requester_id integer NOT NULL, 9 | confirmer_id integer, 10 | request_date timestamp without time zone NOT NULL, 11 | confirm_date timestamp without time zone, 12 | zone_export TEXT, 13 | CONSTRAINT zone_delete_pkey PRIMARY KEY (zone_id), 14 | CONSTRAINT zone_delete_zone_id_fkey FOREIGN KEY (zone_id) REFERENCES zone(id) ON DELETE CASCADE, 15 | CONSTRAINT zone_delete_requester_id_fkey FOREIGN KEY (requester_id) REFERENCES "user"(id), 16 | CONSTRAINT zone_delete_confirmer_id_fkey FOREIGN KEY (confirmer_id) REFERENCES "user"(id) 17 | ) WITH (OIDS=FALSE) 18 | '); 19 | -------------------------------------------------------------------------------- /migrations/006.php: -------------------------------------------------------------------------------- 1 | database->exec("ALTER TYPE auth_realm ADD VALUE 'PHP_AUTH'"); 6 | -------------------------------------------------------------------------------- /model/change.php: -------------------------------------------------------------------------------- 1 | before; 34 | $after = $change->after; 35 | $deleted = (int)!is_null($before); 36 | $added = (int)!is_null($after); 37 | $stmt = $this->database->prepare('INSERT INTO "change" (changeset_id, before, after) VALUES (?, ?, ?)'); 38 | $stmt->bindParam(1, $this->id, PDO::PARAM_INT); 39 | $stmt->bindParam(2, $before, PDO::PARAM_LOB); 40 | $stmt->bindParam(3, $after, PDO::PARAM_LOB); 41 | $stmt->execute(); 42 | $change->id = $this->database->lastInsertId('change_id_seq'); 43 | $stmt = $this->database->prepare('UPDATE "changeset" SET added = added + ?, deleted = deleted + ? WHERE id = ?'); 44 | $stmt->bindParam(1, $added, PDO::PARAM_INT); 45 | $stmt->bindParam(2, $deleted, PDO::PARAM_INT); 46 | $stmt->bindParam(3, $this->id, PDO::PARAM_INT); 47 | $stmt->execute(); 48 | } 49 | 50 | /** 51 | * List all changes in this changeset 52 | * @return array of Change objects 53 | */ 54 | public function list_changes() { 55 | $stmt = $this->database->prepare('SELECT * FROM "change" WHERE changeset_id = ?'); 56 | $stmt->bindParam(1, $this->id, PDO::PARAM_INT); 57 | $stmt->execute(); 58 | $changes = array(); 59 | while($row = $stmt->fetch(PDO::FETCH_ASSOC)) { 60 | if(!is_null($row['before'])) $row['before'] = stream_get_contents($row['before']); 61 | if(!is_null($row['after'])) $row['after'] = stream_get_contents($row['after']); 62 | $changes[] = new Change($row['id'], $row); 63 | } 64 | return $changes; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /model/comment.php: -------------------------------------------------------------------------------- 1 | data = $data; 35 | } 36 | 37 | /** 38 | * Magic setter method - store data in local data object. 39 | * @param string $field to update 40 | * @param mixed $value to store in field 41 | */ 42 | public function __set($field, $value) { 43 | $this->data->{$field} = $value; 44 | } 45 | 46 | /** 47 | * Magic getter method - retrieve data from local data object. 48 | * @param string $field to retrieve 49 | * @return mixed data stored in field 50 | */ 51 | public function __get($field) { 52 | if(isset($this->data->{$field})) return $this->data->{$field}; 53 | return null; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /model/dbdirectory.php: -------------------------------------------------------------------------------- 1 | database = $database; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /model/migrationdirectory.php: -------------------------------------------------------------------------------- 1 | database->prepare('SELECT MAX(id) FROM migration'); 31 | $stmt->execute(); 32 | list($current_migration) = $stmt->fetch(PDO::FETCH_NUM); 33 | } catch(PDOException $e) { 34 | if($e->errorInfo[0] === '42P01') { 35 | $current_migration = 0; 36 | } else { 37 | throw $e; 38 | } 39 | } 40 | if($current_migration < self::LAST_MIGRATION) { 41 | $this->apply_pending_migrations($current_migration); 42 | } 43 | } 44 | 45 | private function apply_pending_migrations($current_migration) { 46 | openlog('dnsui', LOG_ODELAY, LOG_USER); 47 | for($migration_id = $current_migration + 1; $migration_id <= self::LAST_MIGRATION; $migration_id++) { 48 | $filename = str_pad($migration_id, 3, '0', STR_PAD_LEFT).'.php'; 49 | syslog(LOG_INFO, "migration={$filename};object=database;action=apply"); 50 | $migration_name = $filename; 51 | include('migrations/'.$filename); 52 | $stmt = $this->database->prepare('INSERT INTO migration VALUES (?, ?, NOW())'); 53 | $stmt->bindParam(1, $migration_id, PDO::PARAM_INT); 54 | $stmt->bindParam(2, $migration_name, PDO::PARAM_STR); 55 | $stmt->execute(); 56 | } 57 | closelog(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /model/nstemplate.php: -------------------------------------------------------------------------------- 1 | database = $database; 56 | $this->active_user = $active_user; 57 | $this->id = $id; 58 | $this->data = array(); 59 | foreach($preload_data as $field => $value) { 60 | $this->data[$field] = $value; 61 | } 62 | if(is_null($this->id)) $this->dirty = true; 63 | } 64 | 65 | /** 66 | * Magic getter method - return the value of the specified field. Retrieve the row from the 67 | * database if we do not have data for that field yet. 68 | * @param string $field name of field to retrieve 69 | * @return mixed data stored in field 70 | * @throws Exception if the row or the field does not exist in the database 71 | */ 72 | public function &__get($field) { 73 | if(!array_key_exists($field, $this->data)) { 74 | // We don't have a value for this field yet 75 | if(is_null($this->id)) { 76 | // Record is not yet in the database - nothing to retrieve 77 | $result = null; 78 | return $result; 79 | } 80 | // Attempt to get data from database 81 | $stmt = $this->database->prepare("SELECT * FROM \"$this->table\" WHERE {$this->idfield} = ?"); 82 | $stmt->bindParam(1, $this->id, PDO::PARAM_INT); 83 | $stmt->execute(); 84 | 85 | if($stmt->rowCount() != 1) { 86 | throw new Exception("Unexpected number of rows returned ({$stmt->num_rows}), expected exactly 1. Table:{$this->table}, ID field: {$this->idfield}, ID: {$this->id}"); 87 | } 88 | $data = $stmt->fetch(PDO::FETCH_ASSOC); 89 | // Populate data array for fields we do not already have a value for 90 | foreach($data as $f => $v) { 91 | if(!isset($this->data[$f])) { 92 | $this->data[$f] = $v; 93 | } 94 | } 95 | if(!array_key_exists($field, $this->data)) { 96 | // We still don't have a value, so this field doesn't exist in the database 97 | throw new Exception("Field $field does not exist in {$this->table} table."); 98 | } 99 | } 100 | return $this->data[$field]; 101 | } 102 | 103 | /** 104 | * Magic setter method - store the updated value and set the record as dirty. 105 | * @param string $field name of field 106 | * @param mixed $value data to store in field 107 | */ 108 | public function __set($field, $value) { 109 | $this->data[$field] = $value; 110 | $this->dirty = true; 111 | if($field == $this->idfield) $this->id = $value; 112 | } 113 | 114 | /** 115 | * Update the database with all fields that have been modified. 116 | * @return array of StdClass detailing actual updates that were applied 117 | * @throws UniqueKeyViolationException if the update violated a unique key on the table 118 | */ 119 | public function update() { 120 | $stmt = $this->database->prepare("SELECT * FROM \"$this->table\" WHERE {$this->idfield} = ?"); 121 | $stmt->bindParam(1, $this->id, PDO::PARAM_INT); 122 | $stmt->execute(); 123 | if(!($row = $stmt->fetch(PDO::FETCH_ASSOC))) { 124 | throw new Exception("Record not found in database"); 125 | } 126 | $updates = array(); 127 | $fields = array(); 128 | $values = array(); 129 | $types = array(); 130 | foreach($row as $field => $value) { 131 | if(array_key_exists($field, $this->data) && $this->data[$field] != $value) { 132 | $update = new StdClass; 133 | $update->field = $field; 134 | $update->old_value = $value; 135 | $update->new_value = $this->data[$field]; 136 | $updates[] = $update; 137 | $fields[] = "\"$field\" = :$field"; 138 | } 139 | } 140 | if(!empty($updates)) { 141 | try { 142 | $stmt = $this->database->prepare("UPDATE \"$this->table\" SET ".implode(', ', $fields)." WHERE {$this->idfield} = :id"); 143 | foreach($updates as $update) { 144 | $stmt->bindParam($update->field, $update->new_value, PDO::PARAM_STR); 145 | } 146 | $stmt->bindParam('id', $this->id, PDO::PARAM_INT); 147 | $stmt->execute(); 148 | } catch(mysqli_sql_exception $e) { 149 | if($e->getCode() == 1062) { 150 | // Duplicate entry 151 | $message = $e->getMessage(); 152 | if(preg_match("/^Duplicate entry '(.*)' for key '(.*)'$/", $message, $matches)) { 153 | $ne = new UniqueKeyViolationException($e->getMessage()); 154 | $ne->fields = explode(',', $matches[2]); 155 | $ne->values = explode(',', $matches[1]); 156 | throw $ne; 157 | } 158 | } 159 | throw $e; 160 | } 161 | } 162 | $this->dirty = false; 163 | return $updates; 164 | } 165 | } 166 | 167 | class UniqueKeyViolationException extends Exception { 168 | /** 169 | * Fields involved in the unique key conflict 170 | */ 171 | public $fields; 172 | /** 173 | * Values that conflicted 174 | */ 175 | public $values; 176 | } 177 | -------------------------------------------------------------------------------- /model/replicationtype.php: -------------------------------------------------------------------------------- 1 | database->prepare(' 28 | SELECT replication_type.*, CASE WHEN config.id IS NULL THEN 0 ELSE 1 END AS default 29 | FROM replication_type 30 | LEFT JOIN config ON config.default_replication_type = replication_type.id 31 | ORDER BY name 32 | '); 33 | $stmt->execute(); 34 | $repltypes = array(); 35 | while($row = $stmt->fetch(PDO::FETCH_ASSOC)) { 36 | $repltypes[] = new ReplicationType($row['id'], $row); 37 | } 38 | return $repltypes; 39 | } 40 | 41 | /** 42 | * Get a ReplicationType from the database by its id. 43 | * @param int $id of template 44 | * @return ReplicationType with specified id 45 | * @throws ReplicationTypeNotFound if no ReplicationType with that id exists 46 | */ 47 | public function get_replication_type_by_id($id) { 48 | $stmt = $this->database->prepare(' 49 | SELECT replication_type.*, CASE WHEN config.id IS NULL THEN 0 ELSE 1 END AS default 50 | FROM replication_type 51 | LEFT JOIN config ON config.default_replication_type = replication_type.id 52 | WHERE replication_type.id = ? 53 | '); 54 | $stmt->bindParam(1, $id, PDO::PARAM_INT); 55 | $stmt->execute(); 56 | if($row = $stmt->fetch(PDO::FETCH_ASSOC)) { 57 | return new ReplicationType($row['id'], $row); 58 | } else { 59 | throw new ReplicationTypeNotFound; 60 | } 61 | } 62 | 63 | /** 64 | * Set the provided replication type as the default for new zones. 65 | * @param ReplicationType $type to be set as default 66 | */ 67 | public function set_default_replication_type(ReplicationType $type = null) { 68 | $stmt = $this->database->prepare('UPDATE config SET default_replication_type = ?'); 69 | if(is_null($type)) { 70 | $stmt->bindParam(1, $type, PDO::PARAM_INT); 71 | } else { 72 | $stmt->bindParam(1, $type->id, PDO::PARAM_INT); 73 | } 74 | $stmt->execute(); 75 | } 76 | } 77 | 78 | class ReplicationTypeNotFound extends RuntimeException {} 79 | -------------------------------------------------------------------------------- /model/resourcerecord.php: -------------------------------------------------------------------------------- 1 | data = $data; 35 | } 36 | 37 | /** 38 | * Magic setter method - store data in local data object. 39 | * @param string $field to update 40 | * @param mixed $value to store in field 41 | */ 42 | public function __set($field, $value) { 43 | $this->data->{$field} = $value; 44 | } 45 | 46 | /** 47 | * Magic getter method - retrieve data from local data object. 48 | * @param string $field to retrieve 49 | * @return mixed data stored in field 50 | */ 51 | public function __get($field) { 52 | if(isset($this->data->{$field})) return $this->data->{$field}; 53 | return null; 54 | } 55 | } 56 | 57 | class ResourceRecordInvalid extends Exception {} 58 | -------------------------------------------------------------------------------- /model/resourcerecordset.php: -------------------------------------------------------------------------------- 1 | data = $data; 42 | } 43 | 44 | /** 45 | * Magic setter method - store data in local data array. 46 | * @param string $field to update 47 | * @param mixed $value to store in field 48 | */ 49 | public function __set($field, $value) { 50 | $this->data[$field] = $value; 51 | } 52 | 53 | /** 54 | * Magic getter method - retrieve data from local data array. 55 | * @param string $field to retrieve 56 | * @return mixed data stored in field 57 | */ 58 | public function __get($field) { 59 | if(isset($this->data[$field])) return $this->data[$field]; 60 | return null; 61 | } 62 | 63 | /** 64 | * Add a ResourceRecord to this recordset 65 | * @param ResourceRecord $rr to add 66 | */ 67 | public function add_resource_record(ResourceRecord $rr) { 68 | $this->rrs[] = $rr; 69 | } 70 | 71 | /** 72 | * List all ResourceRecord objects in this recordset 73 | * @return array of ResourceRecord objects 74 | */ 75 | public function &list_resource_records() { 76 | return $this->rrs; 77 | } 78 | 79 | /** 80 | * Empty the list of ResourceRecord objects in this recordset 81 | */ 82 | public function clear_resource_records() { 83 | $this->rrs = array(); 84 | } 85 | 86 | /** 87 | * Add a Comment to this recordset 88 | * @param Comment $comment to add 89 | */ 90 | public function add_comment(Comment $comment) { 91 | $this->comments[] = $comment; 92 | } 93 | 94 | /** 95 | * List all Comment objects associated with this recordset 96 | * @return array of Comment objects 97 | */ 98 | public function list_comments() { 99 | return $this->comments; 100 | } 101 | 102 | /** 103 | * For legacy purposes when we allowed multiple comments per RRset in the UI. 104 | * This function joins the text of all associated comments. 105 | * @return string joined text 106 | */ 107 | public function merge_comment_text() { 108 | $text = ''; 109 | foreach($this->comments as $comment) { 110 | $text = trim($text.' '.$comment->content); 111 | } 112 | return $text; 113 | } 114 | 115 | /** 116 | * Return a single string containing all contents of records in the RRset 117 | */ 118 | public function merge_content_text() { 119 | $array = array(); 120 | foreach($this->list_resource_records() as $record) { 121 | $array[] = $record->content; 122 | } 123 | return english_list($array); 124 | } 125 | 126 | /** 127 | * Empty the list of Comment objects associated in this recordset. 128 | */ 129 | public function clear_comments() { 130 | $this->comments = array(); 131 | } 132 | 133 | /** 134 | * Rename this recordset to a different name/type. 135 | * As far as PowerDNS is concerned we will be deleting the old RRset and creating a new one. 136 | * But for changelog purposes we can show this as a rename. 137 | * @param string $name new name of the RRset 138 | * @param string $type new type of the RRset 139 | */ 140 | public function rename($name, $type) { 141 | if($this->data['name'] == $name && $this->data['type'] == $type) return false; 142 | $this->data['name'] = $name; 143 | $this->data['type'] = $type; 144 | return true; 145 | } 146 | } 147 | 148 | class ResourceRecordSetInvalid extends Exception {} 149 | -------------------------------------------------------------------------------- /model/soatemplate.php: -------------------------------------------------------------------------------- 1 | data['class'])) $this->data['class'] = 'success'; 33 | if(!isset($this->data['escaping'])) $this->data['escaping'] = ESC_HTML; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /model/userdirectory.php: -------------------------------------------------------------------------------- 1 | ldap = $ldap; 35 | $this->cache_uid = array(); 36 | } 37 | 38 | /** 39 | * Create the new user in the database. 40 | * @param User $user object to add 41 | */ 42 | public function add_user(User $user) { 43 | $stmt = $this->database->prepare('INSERT INTO "user" (uid, name, email, active, admin, auth_realm) VALUES (?, ?, ?, ?, ?, ?)'); 44 | $stmt->bindParam(1, $user->uid, PDO::PARAM_INT); 45 | $stmt->bindParam(2, $user->name, PDO::PARAM_STR); 46 | $stmt->bindParam(3, $user->email, PDO::PARAM_STR); 47 | $stmt->bindParam(4, $user->active, PDO::PARAM_INT); 48 | $stmt->bindParam(5, $user->admin, PDO::PARAM_INT); 49 | $stmt->bindParam(6, $user->auth_realm, PDO::PARAM_INT); 50 | try { 51 | $stmt->execute(); 52 | $user->id = $this->database->lastInsertId('user_id_seq'); 53 | } catch(PDOException $e) { 54 | if($e->getCode() == 23505) throw new UserAlreadyExistsException('A user already exists with uid '.$user->uid); 55 | throw $e; 56 | } 57 | } 58 | 59 | /** 60 | * Get a user from the database by its ID. 61 | * @param int $id of user 62 | * @return User with specified ID 63 | * @throws UserNotFoundException if no user with that ID exists 64 | */ 65 | public function get_user_by_id($id) { 66 | $stmt = $this->database->prepare('SELECT * FROM "user" WHERE id = ?'); 67 | $stmt->bindParam(1, $id, PDO::PARAM_INT); 68 | $stmt->execute(); 69 | if($row = $stmt->fetch(PDO::FETCH_ASSOC)) { 70 | $user = new User($row['id'], $row); 71 | } else { 72 | throw new UserNotFoundException('User does not exist.'); 73 | } 74 | return $user; 75 | } 76 | 77 | /** 78 | * Get a user from the database by its uid. If it does not exist in the database, retrieve it 79 | * from LDAP and store in the database. 80 | * @param string $uid of user 81 | * @return User with specified uid 82 | * @throws UserNotFoundException if no user with that uid exists 83 | */ 84 | public function get_user_by_uid($uid) { 85 | global $config; 86 | if(isset($config['authentication']['user_case_sensitive']) && $config['authentication']['user_case_sensitive'] == 0) { 87 | $uid = mb_strtolower($uid); 88 | $sql_statement = 'SELECT * FROM "user" WHERE lower(uid) = ?'; 89 | } else { 90 | $sql_statement = 'SELECT * FROM "user" WHERE uid = ?'; 91 | } 92 | 93 | if(isset($this->cache_uid[$uid])) { 94 | return $this->cache_uid[$uid]; 95 | } 96 | $stmt = $this->database->prepare($sql_statement); 97 | $stmt->bindParam(1, $uid, PDO::PARAM_STR); 98 | $stmt->execute(); 99 | if($row = $stmt->fetch(PDO::FETCH_ASSOC)) { 100 | $user = new User($row['id'], $row); 101 | $this->cache_uid[$uid] = $user; 102 | } else { 103 | $user = new User; 104 | $user->uid = $uid; 105 | $this->cache_uid[$uid] = $user; 106 | $user->get_details(); 107 | $this->add_user($user); 108 | } 109 | return $user; 110 | } 111 | 112 | /** 113 | * List all users in the database. 114 | * @param array $include list of extra data to include in response - currently unused 115 | * @param array $filter list of field/value pairs to filter results on 116 | * @return array of User objects 117 | */ 118 | public function list_users($include = array(), $filter = array()) { 119 | // WARNING: The search query is not parameterized - be sure to properly escape all input 120 | $fields = array('"user".*'); 121 | $joins = array(); 122 | $where = array(); 123 | foreach($filter as $field => $value) { 124 | if($value) { 125 | switch($field) { 126 | case 'uid': 127 | $where[] = "uid REGEXP ".$this->database->quote($value); 128 | break; 129 | } 130 | } 131 | } 132 | $stmt = $this->database->prepare(' 133 | SELECT '.implode(', ', $fields).' 134 | FROM "user" '.implode(" ", $joins).' 135 | '.(count($where) == 0 ? '' : 'WHERE ('.implode(') AND (', $where).')').' 136 | GROUP BY "user".id 137 | ORDER BY "user".uid 138 | '); 139 | $stmt->execute(); 140 | $users = array(); 141 | while($row = $stmt->fetch(PDO::FETCH_ASSOC)) { 142 | $users[] = new User($row['id'], $row); 143 | } 144 | return $users; 145 | } 146 | } 147 | 148 | class UserNotFoundException extends Exception {} 149 | class UserDataSourceException extends Exception {} 150 | class UserAlreadyExistsException extends Exception {} 151 | -------------------------------------------------------------------------------- /model/zoneaccess.php: -------------------------------------------------------------------------------- 1 | template = $template; 28 | $this->data = new StdClass; 29 | $this->data->menu_items = array(); 30 | $this->data->menu_items['Zones'] = '/zones'; 31 | if(is_object($active_user) && $active_user->admin) { 32 | $this->data->menu_items['Templates'] = array(); 33 | $this->data->menu_items['Templates']['SOA templates'] = '/templates/soa'; 34 | $this->data->menu_items['Templates']['Nameserver templates'] = '/templates/ns'; 35 | $this->data->menu_items['Users'] = '/users'; 36 | $this->data->menu_items['Settings'] = '/settings'; 37 | } 38 | $this->data->relative_request_url = $relative_request_url; 39 | $this->data->active_user = $active_user; 40 | $this->data->web_config = $config['web']; 41 | $this->data->email_config = $config['email']; 42 | if(is_object($active_user) && $active_user->developer) { 43 | $this->data->database = $database; 44 | } 45 | } 46 | public function set_by_array($array, $prefix = '') { 47 | foreach($array as $item => $data) { 48 | $this->setData($prefix.$item, $data); 49 | } 50 | } 51 | public function set($item, $data) { 52 | $this->data->$item = $data; 53 | } 54 | public function get($item) { 55 | if(isset($this->data->$item)) { 56 | if(is_object($this->data->$item) && get_class($this->data->$item) == 'PageSection') { 57 | return $this->data->$item->generate(); 58 | } else { 59 | return $this->data->$item; 60 | } 61 | } else { 62 | return null; 63 | } 64 | } 65 | public function generate() { 66 | ob_start(); 67 | $data = $this->data; 68 | include_once(path_join('templates', 'functions.php')); 69 | include(path_join('templates', $this->template.'.php')); 70 | $output = ob_get_contents(); 71 | ob_end_clean(); 72 | return $output; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /powerdns.php: -------------------------------------------------------------------------------- 1 | api_key = $api_key; 25 | parent::__construct($base_url); 26 | } 27 | 28 | public function get($url, $data = array(), $headers=array()) { 29 | $headers['X-API-Key'] = $this->api_key; 30 | return parent::get($url, $data, $headers); 31 | } 32 | 33 | public function head($url, $headers = array()) { 34 | $headers['X-API-Key'] = $this->api_key; 35 | return parent::head($url, $headers); 36 | } 37 | 38 | public function post($url, $data, $headers = array()) { 39 | $headers['X-API-Key'] = $this->api_key; 40 | return parent::post($url, $data, $headers); 41 | } 42 | 43 | public function put($url, $data, $headers = array()) { 44 | $headers['X-API-Key'] = $this->api_key; 45 | return parent::put($url, $data, $headers); 46 | } 47 | 48 | public function patch($url, $data, $headers = array()) { 49 | $headers['X-API-Key'] = $this->api_key; 50 | return parent::patch($url, $data, $headers); 51 | } 52 | 53 | public function delete($url, $headers = array()) { 54 | $headers['X-API-Key'] = $this->api_key; 55 | return parent::delete($url, $headers); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /public_html/book_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/operasoftware/dns-ui/e5c73d5ad0384091a97fded483261ba0674eee6e/public_html/book_next.png -------------------------------------------------------------------------------- /public_html/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/operasoftware/dns-ui/e5c73d5ad0384091a97fded483261ba0674eee6e/public_html/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public_html/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/operasoftware/dns-ui/e5c73d5ad0384091a97fded483261ba0674eee6e/public_html/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public_html/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/operasoftware/dns-ui/e5c73d5ad0384091a97fded483261ba0674eee6e/public_html/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public_html/bootstrap/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/operasoftware/dns-ui/e5c73d5ad0384091a97fded483261ba0674eee6e/public_html/bootstrap/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /public_html/bootstrap/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /public_html/init.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /public_html/ipaddr/SOURCE: -------------------------------------------------------------------------------- 1 | https://github.com/whitequark/ipaddr.js -------------------------------------------------------------------------------- /public_html/ipaddr/ipaddr.min.js: -------------------------------------------------------------------------------- 1 | (function(){var r,t,n,e,i,o,a,s;t={},s=this,"undefined"!=typeof module&&null!==module&&module.exports?module.exports=t:s.ipaddr=t,a=function(r,t,n,e){var i,o;if(r.length!==t.length)throw new Error("ipaddr: cannot match CIDR for objects with different lengths");for(i=0;e>0;){if(o=n-e,0>o&&(o=0),r[i]>>o!==t[i]>>o)return!1;e-=n,i+=1}return!0},t.subnetMatch=function(r,t,n){var e,i,o,a,s;null==n&&(n="unicast");for(e in t)for(i=t[e],!i[0]||i[0]instanceof Array||(i=[i]),a=0,s=i.length;s>a;a++)if(o=i[a],r.match.apply(r,o))return e;return n},t.IPv4=function(){function r(r){var t,n,e;if(4!==r.length)throw new Error("ipaddr: ipv4 octet count should be 4");for(n=0,e=r.length;e>n;n++)if(t=r[n],!(t>=0&&255>=t))throw new Error("ipaddr: ipv4 octet is a byte");this.octets=r}return r.prototype.kind=function(){return"ipv4"},r.prototype.toString=function(){return this.octets.join(".")},r.prototype.toByteArray=function(){return this.octets.slice(0)},r.prototype.match=function(r,t){var n;if(void 0===t&&(n=r,r=n[0],t=n[1]),"ipv4"!==r.kind())throw new Error("ipaddr: cannot match ipv4 address with non-ipv4 one");return a(this.octets,r.octets,8,t)},r.prototype.SpecialRanges={unspecified:[[new r([0,0,0,0]),8]],broadcast:[[new r([255,255,255,255]),32]],multicast:[[new r([224,0,0,0]),4]],linkLocal:[[new r([169,254,0,0]),16]],loopback:[[new r([127,0,0,0]),8]],"private":[[new r([10,0,0,0]),8],[new r([172,16,0,0]),12],[new r([192,168,0,0]),16]],reserved:[[new r([192,0,0,0]),24],[new r([192,0,2,0]),24],[new r([192,88,99,0]),24],[new r([198,51,100,0]),24],[new r([203,0,113,0]),24],[new r([240,0,0,0]),4]]},r.prototype.range=function(){return t.subnetMatch(this,this.SpecialRanges)},r.prototype.toIPv4MappedAddress=function(){return t.IPv6.parse("::ffff:"+this.toString())},r.prototype.prefixLengthFromSubnetMask=function(){var r,t,n,e,i,o,a;for(o={0:8,128:7,192:6,224:5,240:4,248:3,252:2,254:1,255:0},r=0,e=!1,t=a=3;a>=0;t=a+=-1){if(n=this.octets[t],!(n in o))return null;if(i=o[n],e&&0!==i)return null;8!==i&&(e=!0),r+=i}return 32-r},r}(),n="(0?\\d+|0x[a-f0-9]+)",e={fourOctet:new RegExp("^"+n+"\\."+n+"\\."+n+"\\."+n+"$","i"),longValue:new RegExp("^"+n+"$","i")},t.IPv4.parser=function(r){var t,n,i,o,a;if(n=function(r){return"0"===r[0]&&"x"!==r[1]?parseInt(r,8):parseInt(r)},t=r.match(e.fourOctet))return function(){var r,e,o,a;for(o=t.slice(1,6),a=[],r=0,e=o.length;e>r;r++)i=o[r],a.push(n(i));return a}();if(t=r.match(e.longValue)){if(a=n(t[1]),a>4294967295||0>a)throw new Error("ipaddr: address outside defined range");return function(){var r,t;for(t=[],o=r=0;24>=r;o=r+=8)t.push(a>>o&255);return t}().reverse()}return null},t.IPv6=function(){function r(r){var t,n,e,i,o,a;if(16===r.length)for(this.parts=[],t=e=0;14>=e;t=e+=2)this.parts.push(r[t]<<8|r[t+1]);else{if(8!==r.length)throw new Error("ipaddr: ipv6 part count should be 8 or 16");this.parts=r}for(a=this.parts,i=0,o=a.length;o>i;i++)if(n=a[i],!(n>=0&&65535>=n))throw new Error("ipaddr: ipv6 part should fit to two octets")}return r.prototype.kind=function(){return"ipv6"},r.prototype.toString=function(){var r,t,n,e,i,o,a;for(i=function(){var r,n,e,i;for(e=this.parts,i=[],r=0,n=e.length;n>r;r++)t=e[r],i.push(t.toString(16));return i}.call(this),r=[],n=function(t){return r.push(t)},e=0,o=0,a=i.length;a>o;o++)switch(t=i[o],e){case 0:n("0"===t?"":t),e=1;break;case 1:"0"===t?e=2:n(t);break;case 2:"0"!==t&&(n(""),n(t),e=3);break;case 3:n(t)}return 2===e&&(n(""),n("")),r.join(":")},r.prototype.toByteArray=function(){var r,t,n,e,i;for(r=[],i=this.parts,n=0,e=i.length;e>n;n++)t=i[n],r.push(t>>8),r.push(255&t);return r},r.prototype.toNormalizedString=function(){var r;return function(){var t,n,e,i;for(e=this.parts,i=[],t=0,n=e.length;n>t;t++)r=e[t],i.push(r.toString(16));return i}.call(this).join(":")},r.prototype.match=function(r,t){var n;if(void 0===t&&(n=r,r=n[0],t=n[1]),"ipv6"!==r.kind())throw new Error("ipaddr: cannot match ipv6 address with non-ipv6 one");return a(this.parts,r.parts,16,t)},r.prototype.SpecialRanges={unspecified:[new r([0,0,0,0,0,0,0,0]),128],linkLocal:[new r([65152,0,0,0,0,0,0,0]),10],multicast:[new r([65280,0,0,0,0,0,0,0]),8],loopback:[new r([0,0,0,0,0,0,0,1]),128],uniqueLocal:[new r([64512,0,0,0,0,0,0,0]),7],ipv4Mapped:[new r([0,0,0,0,0,65535,0,0]),96],rfc6145:[new r([0,0,0,0,65535,0,0,0]),96],rfc6052:[new r([100,65435,0,0,0,0,0,0]),96],"6to4":[new r([8194,0,0,0,0,0,0,0]),16],teredo:[new r([8193,0,0,0,0,0,0,0]),32],reserved:[[new r([8193,3512,0,0,0,0,0,0]),32]]},r.prototype.range=function(){return t.subnetMatch(this,this.SpecialRanges)},r.prototype.isIPv4MappedAddress=function(){return"ipv4Mapped"===this.range()},r.prototype.toIPv4Address=function(){var r,n,e;if(!this.isIPv4MappedAddress())throw new Error("ipaddr: trying to convert a generic ipv6 address to ipv4");return e=this.parts.slice(-2),r=e[0],n=e[1],new t.IPv4([r>>8,255&r,n>>8,255&n])},r}(),i="(?:[0-9a-f]+::?)+",o={"native":new RegExp("^(::)?("+i+")?([0-9a-f]+)?(::)?$","i"),transitional:new RegExp("^((?:"+i+")|(?:::)(?:"+i+")?)"+(""+n+"\\."+n+"\\."+n+"\\."+n+"$"),"i")},r=function(r,t){var n,e,i,o,a;if(r.indexOf("::")!==r.lastIndexOf("::"))return null;for(n=0,e=-1;(e=r.indexOf(":",e+1))>=0;)n++;if("::"===r.substr(0,2)&&n--,"::"===r.substr(-2,2)&&n--,n>t)return null;for(a=t-n,o=":";a--;)o+="0:";return r=r.replace("::",o),":"===r[0]&&(r=r.slice(1)),":"===r[r.length-1]&&(r=r.slice(0,-1)),function(){var t,n,e,o;for(e=r.split(":"),o=[],t=0,n=e.length;n>t;t++)i=e[t],o.push(parseInt(i,16));return o}()},t.IPv6.parser=function(t){var n,e;return t.match(o["native"])?r(t,8):(n=t.match(o.transitional))&&(e=r(n[1].slice(0,-1),6))?(e.push(parseInt(n[2])<<8|parseInt(n[3])),e.push(parseInt(n[4])<<8|parseInt(n[5])),e):null},t.IPv4.isIPv4=t.IPv6.isIPv6=function(r){return null!==this.parser(r)},t.IPv4.isValid=function(r){var t;try{return new this(this.parser(r)),!0}catch(n){return t=n,!1}},t.IPv6.isValid=function(r){var t;if("string"==typeof r&&-1===r.indexOf(":"))return!1;try{return new this(this.parser(r)),!0}catch(n){return t=n,!1}},t.IPv4.parse=t.IPv6.parse=function(r){var t;if(t=this.parser(r),null===t)throw new Error("ipaddr: string is not formatted like ip address");return new this(t)},t.IPv4.parseCIDR=function(r){var t,n;if((n=r.match(/^(.+)\/(\d+)$/))&&(t=parseInt(n[2]),t>=0&&32>=t))return[this.parse(n[1]),t];throw new Error("ipaddr: string is not formatted like an IPv4 CIDR range")},t.IPv6.parseCIDR=function(r){var t,n;if((n=r.match(/^(.+)\/(\d+)$/))&&(t=parseInt(n[2]),t>=0&&128>=t))return[this.parse(n[1]),t];throw new Error("ipaddr: string is not formatted like an IPv6 CIDR range")},t.isValid=function(r){return t.IPv6.isValid(r)||t.IPv4.isValid(r)},t.parse=function(r){if(t.IPv6.isValid(r))return t.IPv6.parse(r);if(t.IPv4.isValid(r))return t.IPv4.parse(r);throw new Error("ipaddr: the address has neither IPv6 nor IPv4 format")},t.parseCIDR=function(r){var n;try{return t.IPv6.parseCIDR(r)}catch(e){n=e;try{return t.IPv4.parseCIDR(r)}catch(e){throw n=e,new Error("ipaddr: the address has neither IPv6 nor IPv4 CIDR format")}}},t.fromByteArray=function(r){var n;if(n=r.length,4===n)return new t.IPv4(r);if(16===n)return new t.IPv6(r);throw new Error("ipaddr: the binary input is neither an IPv6 nor IPv4 address")},t.process=function(r){var t;return t=this.parse(r),"ipv6"===t.kind()&&t.isIPv4MappedAddress()?t.toIPv4Address():t}}).call(this); 2 | -------------------------------------------------------------------------------- /public_html/logo-header-opera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/operasoftware/dns-ui/e5c73d5ad0384091a97fded483261ba0674eee6e/public_html/logo-header-opera.png -------------------------------------------------------------------------------- /public_html/screenshot-changelog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/operasoftware/dns-ui/e5c73d5ad0384091a97fded483261ba0674eee6e/public_html/screenshot-changelog.png -------------------------------------------------------------------------------- /public_html/screenshot-zoneedit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/operasoftware/dns-ui/e5c73d5ad0384091a97fded483261ba0674eee6e/public_html/screenshot-zoneedit.png -------------------------------------------------------------------------------- /public_html/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | ## 3 | ## Copyright 2013-2018 Opera Software AS 4 | ## 5 | ## Licensed under the Apache License, Version 2.0 (the "License"); 6 | ## you may not use this file except in compliance with the License. 7 | ## You may obtain a copy of the License at 8 | ## 9 | ## http://www.apache.org/licenses/LICENSE-2.0 10 | ## 11 | ## Unless required by applicable law or agreed to in writing, software 12 | ## distributed under the License is distributed on an "AS IS" BASIS, 13 | ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ## See the License for the specific language governing permissions and 15 | ## limitations under the License. 16 | ## 17 | */ 18 | html, 19 | body { 20 | height: 100%; 21 | /* The html and body elements cannot have any padding or margin. */ 22 | } 23 | 24 | /* Wrapper for page content to push down footer */ 25 | #wrap { 26 | min-height: 100%; 27 | height: auto; 28 | /* Negative indent footer by its height */ 29 | margin: 0 auto -60px; 30 | /* Pad bottom by footer height */ 31 | padding: 0 0 60px; 32 | } 33 | #wrap > .container { 34 | padding: 60px 15px 0; 35 | } 36 | #content *:first-child { 37 | margin-top: 0; 38 | } 39 | .nav-tabs { 40 | margin-bottom: 20px; 41 | } 42 | #footer { 43 | height: 60px; 44 | background-color: #f5f5f5; 45 | } 46 | #footer > .container { 47 | padding: 20px 15px 0 15px; 48 | } 49 | .navbar-brand img { 50 | height: 100%; 51 | float: left; 52 | margin-right: 1em; 53 | } 54 | .panel-group + p { 55 | margin-top: 1em; 56 | } 57 | ul.plain { 58 | list-style-type: none; 59 | padding: 0; 60 | margin: 0; 61 | } 62 | table.zonelist td.name { 63 | text-align: right; 64 | } 65 | table.zonelist tr.filtered { 66 | display: none; 67 | } 68 | form.zoneedit div.filter { 69 | margin-bottom: 2em; 70 | margin-right: 1em; 71 | } 72 | table.rrsets { 73 | table-layout: fixed; 74 | } 75 | table.rrsets tr td input[type=text] { 76 | padding: 0; 77 | line-height: 12px; 78 | } 79 | table.rrsets tr td input, 80 | table.rrsets tr td select, 81 | table.rrsets tr td button { 82 | width: 100%; 83 | background-color: white; 84 | color: #555; 85 | } 86 | table.rrsets td.name, 87 | table.rrsets td.type, 88 | table.rrsets td.ttl, 89 | table.rrsets td.content, 90 | table.rrsets td.enabled, 91 | table.rrsets td.comment { 92 | cursor: pointer; 93 | } 94 | table.rrsets td.name:hover, 95 | table.rrsets td.type:hover, 96 | table.rrsets td.ttl:hover, 97 | table.rrsets td.content:hover, 98 | table.rrsets td.enabled:hover, 99 | table.rrsets td.comment:hover { 100 | background-color: #ccf; 101 | } 102 | table.rrsets th.name { 103 | width: 21em; 104 | } 105 | table.rrsets th.type { 106 | width: 7em; 107 | } 108 | table.rrsets th.ttl { 109 | width: 7em; 110 | } 111 | table.rrsets th.content { 112 | width: 21em; 113 | } 114 | table.rrsets th.enabled { 115 | width: 7em; 116 | } 117 | table.rrsets th.actions { 118 | width: 6em; 119 | } 120 | table.rrsets td { 121 | overflow: hidden; 122 | text-overflow: ellipsis; 123 | } 124 | table.rrsets td.name, table.rrsets td.name input { 125 | text-align: right; 126 | } 127 | table.rrsets tr.disabled td.enabled { 128 | background-color: #ddd; 129 | } 130 | table.rrsets tr.disabled td.enabled:hover { 131 | background-color: #ccd; 132 | } 133 | table.rrsets tr.delete td.content, 134 | table.rrsets tr.delete td.enabled, 135 | table.rrsets tr.delete td.actions { 136 | background-color: #f2dede; 137 | } 138 | table.rrsets tr.rrset-delete td { 139 | background-color: #f2dede; 140 | } 141 | table.rrsets tr.filtered { 142 | display: none; 143 | } 144 | #reverse4 > table.zonelist td.name, 145 | #reverse6 > table.zonelist td.name, 146 | table.rrsets td.name { 147 | font-family: monospace; 148 | } 149 | table.changepreview { 150 | table-layout: fixed; 151 | } 152 | table.changepreview th.name { 153 | width: 21em; 154 | } 155 | table.changepreview th.type { 156 | width: 7em; 157 | } 158 | table.changepreview th.confirm { 159 | width: 10em; 160 | } 161 | table.changepreview td { 162 | overflow: hidden; 163 | text-overflow: ellipsis; 164 | } 165 | table.changelog tbody tr { 166 | cursor: pointer; 167 | } 168 | table.changelog tbody tr.changeset { 169 | cursor: default; 170 | } 171 | table.changelog table td.content { 172 | word-break: break-all; 173 | } 174 | 175 | #changelog-search { 176 | display: inline; 177 | } 178 | 179 | #changelog-expand-all, #changelog-collapse-all { 180 | float: right; 181 | } 182 | #changelog-collapse-all { 183 | display: none; 184 | } 185 | 186 | input[type=text]:focus:invalid, input[type=email]:focus:invalid, select:focus:invalid, textarea:focus:invalid { 187 | box-shadow: 0 0 5px #d45252; 188 | border-color: #b03535; 189 | } 190 | input[type=text]:focus:valid, input[type=email]:focus:valid, select:focus:valid, textarea:focus:valid { 191 | box-shadow: 0 0 5px #5cd053; 192 | border-color: #28921f; 193 | } 194 | ins { 195 | background-color: #dff0d8; 196 | color: #3c763d; 197 | text-decoration: none; 198 | } 199 | del { 200 | background-color: #f2dede; 201 | color: #a94442; 202 | } 203 | .align-right { 204 | text-align: right; 205 | } 206 | .nowrap { 207 | white-space: nowrap; 208 | } 209 | .zone-hint { 210 | color: #bbb; 211 | } 212 | td:hover .zone-hint { 213 | color: #999; 214 | } 215 | pre.source { 216 | max-height: 30em; 217 | } 218 | 219 | div.stickyHeader { 220 | position: fixed; 221 | top: 51px; 222 | } 223 | div.stickyHeader th { 224 | background-color: white; 225 | } 226 | 227 | /** 228 | * GeSHi (C) 2004 - 2007 Nigel McNie, 2007 - 2008 Benny Baumann 229 | * (http://qbnz.com/highlighter/ and http://geshi.org/) 230 | */ 231 | .javascript {font-family:monospace;} 232 | .javascript .imp {font-weight: bold; color: red;} 233 | .javascript .kw1 {color: #000066; font-weight: bold;} 234 | .javascript .kw2 {color: #003366; font-weight: bold;} 235 | .javascript .kw3 {color: #000066;} 236 | .javascript .kw5 {color: #FF0000;} 237 | .javascript .co1 {color: #006600; font-style: italic;} 238 | .javascript .co2 {color: #009966; font-style: italic;} 239 | .javascript .coMULTI {color: #006600; font-style: italic;} 240 | .javascript .es0 {color: #000099; font-weight: bold;} 241 | .javascript .br0 {color: #009900;} 242 | .javascript .sy0 {color: #339933;} 243 | .javascript .st0 {color: #3366CC;} 244 | .javascript .nu0 {color: #CC0000;} 245 | .javascript .me1 {color: #660066;} 246 | .javascript span.xtra { display:block; } 247 | -------------------------------------------------------------------------------- /requesthandler.php: -------------------------------------------------------------------------------- 1 | get_user_by_uid($_SERVER['PHP_AUTH_USER']); 25 | } elseif(isset($_SERVER['REMOTE_USER'])) { 26 | $active_user = $user_dir->get_user_by_uid($_SERVER['REMOTE_USER']); 27 | } else { 28 | throw new Exception("Not logged in."); 29 | } 30 | 31 | // Work out where we are on the server 32 | $request_url = preg_replace('|(.)/$|', '$1', $_SERVER['REQUEST_URI']); 33 | $relative_request_url = preg_replace('/^'.preg_quote($relative_frontend_base_url, '/').'/', '', $request_url) ?: '/'; 34 | $absolute_request_url = $frontend_root_url.$request_url; 35 | 36 | if(empty($config['web']['enabled'])) { 37 | require('views/error503.php'); 38 | die; 39 | } 40 | 41 | if(!$active_user->active) { 42 | require('views/error403.php'); 43 | } 44 | 45 | if(!empty($_POST)) { 46 | // Check CSRF token 47 | if(isset($_SERVER['HTTP_X_BYPASS_CSRF_PROTECTION']) && $_SERVER['HTTP_X_BYPASS_CSRF_PROTECTION'] == 1) { 48 | // This is being called from script, not a web browser 49 | } elseif(!$active_user->check_csrf_token($_POST['csrf_token'])) { 50 | require('views/csrf.php'); 51 | die; 52 | } 53 | } 54 | 55 | // Route request to the correct view 56 | $router = new Router; 57 | foreach($routes as $path => $service) { 58 | $router->add_route($path, $service); 59 | } 60 | $router->handle_request($relative_request_url); 61 | if(isset($router->view)) { 62 | $view = path_join($base_path, 'views', $router->view.'.php'); 63 | if(file_exists($view)) { 64 | require($view); 65 | } else { 66 | throw new Exception("View file $view missing."); 67 | } 68 | } 69 | 70 | // Handler for uncaught exceptions 71 | function exception_handler($e) { 72 | global $active_user; 73 | if((get_class($e) == 'Pest_Curl_Exec' && $e->getCode() == CURLE_COULDNT_CONNECT) || get_class($e) == 'Pest_Unauthorized') { 74 | require('views/error503_upstream.php'); 75 | die; 76 | } 77 | $error_number = log_exception($e); 78 | while(ob_get_length()) { 79 | ob_end_clean(); 80 | } 81 | require('views/error500.php'); 82 | die; 83 | } 84 | -------------------------------------------------------------------------------- /router.php: -------------------------------------------------------------------------------- 1 | route_vars = array(); 26 | $path = preg_replace_callback('|\\\{([a-z_]+)\\\}|', array($this, 'parse_route_variable'), preg_quote($path, '|')); 27 | $route = new StdClass; 28 | $route->view = $view; 29 | $route->vars = $this->route_vars; 30 | $this->routes[$path] = $route; 31 | } 32 | 33 | private function parse_route_variable($matches) { 34 | $this->route_vars[] = $matches[1]; 35 | return '([^/]*)'; 36 | } 37 | 38 | public function handle_request($request_path) { 39 | $request_path = preg_replace('|\?.*$|', '', $request_path); 40 | foreach($this->routes as $path => $route) { 41 | if(preg_match('|^'.$path.'$|', $request_path, $matches)) { 42 | $this->view = $route->view; 43 | $i = 0; 44 | foreach($route->vars as $var) { 45 | $i++; 46 | if(isset($matches[$i])) { 47 | $this->vars[$var] = urldecode($matches[$i]); 48 | } 49 | } 50 | } 51 | } 52 | if(is_null($this->view)) { 53 | $this->view = 'error404'; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /routes.php: -------------------------------------------------------------------------------- 1 | 'home', 20 | '/api/v2' => 'api', 21 | '/api/v2/{objects}' => 'api', 22 | '/api/v2/{objects}/{id}' => 'api', 23 | '/api/v2/{objects}/{id}/{subobjects}' => 'api', 24 | '/api/v2/{objects}/{id}/{subobjects}/{subid}' => 'api', 25 | '/settings' => 'settings', 26 | '/templates' => 'templates', 27 | '/templates/{type}' => 'templates', 28 | '/templates/{type}/{name}' => 'template', 29 | '/users' => 'users', 30 | '/users/{uid}' => 'user', 31 | '/zones' => 'zones', 32 | '/zones/{name}' => 'zone', 33 | '/zones/{name}/import' => 'zoneimport', 34 | '/zones/{name}/export' => 'zoneexport', 35 | '/zones/{name}/split' => 'zonesplit', 36 | ); 37 | -------------------------------------------------------------------------------- /scripts/create_admin_account.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | auth_realm = 'local'; 35 | $user->uid = $uid; 36 | $user->name = $name; 37 | $user->email = $email; 38 | $user->active = 1; 39 | $user->admin = 1; 40 | try { 41 | $user_dir->add_user($user); 42 | echo "\nAdministrative user $uid created.\n"; 43 | } catch(UserAlreadyExistsException $e) { 44 | echo "\nA user with user ID of $uid already exists.\n"; 45 | } 46 | -------------------------------------------------------------------------------- /scripts/full_git_tracked_export.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | get_user_by_uid('import'); 25 | } catch(UserNotFoundException $e) { 26 | $active_user = new User; 27 | $active_user->uid = 'import'; 28 | $active_user->name = 'Import script'; 29 | $active_user->email = null; 30 | $active_user->auth_realm = 'local'; 31 | $active_user->active = 1; 32 | $active_user->admin = 1; 33 | $active_user->developer = 0; 34 | $user_dir->add_user($active_user); 35 | } 36 | 37 | if($config['git_tracked_export']['enabled'] != 1) { 38 | die('git_tracked_export is not enabled (see config/config.ini)'); 39 | } 40 | 41 | if(empty($argv[1])) { 42 | echo "Usage: full_git_tracked_export.php \n"; 43 | exit(1); 44 | } 45 | $comment = $argv[1]; 46 | 47 | $zones = $zone_dir->list_zones(); 48 | $zone_exports = array(); 49 | foreach($zones as $zone) { 50 | $zone_exports[$zone->name] = $zone->export_as_bind9_format(); 51 | } 52 | $original_dir = getcwd(); 53 | if(chdir($config['git_tracked_export']['path'])) { 54 | foreach($zone_exports as $name => $export) { 55 | $name = DNSZoneName::unqualify($name); 56 | $fh = fopen($name, 'w'); 57 | fwrite($fh, $export); 58 | fclose($fh); 59 | } 60 | exec('LANG=en_US.UTF-8 git add -A'); 61 | exec('LANG=en_US.UTF-8 git commit --author '.escapeshellarg($active_user->name.' <'.$active_user->email.'>').' -m '.escapeshellarg($comment)); 62 | chdir($original_dir); 63 | } 64 | -------------------------------------------------------------------------------- /scripts/ldap_update.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | list_users(); 23 | 24 | foreach($users as $user) { 25 | if($user->auth_realm == 'LDAP') { 26 | try { 27 | $user->get_details_from_ldap(); 28 | $user->update(); 29 | } catch(UserNotFoundException $e) { 30 | $user->active = 0; 31 | $user->update(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /templates/base.php: -------------------------------------------------------------------------------- 1 | get('web_config'); 18 | header('X-Frame-Options: DENY'); 19 | header("Content-Security-Policy: default-src 'self'"); 20 | ?> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | <?php out($this->get('title'))?> 32 | get('head'), ESC_NONE) ?> 33 |
34 | Skip to main content 35 | 74 |
75 | get('alerts') as $alert) { ?> 76 |
77 | 78 | content, $alert->escaping)?> 79 |
80 | 81 | get('content'), ESC_NONE) ?> 82 |
83 |
84 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /templates/csrf.php: -------------------------------------------------------------------------------- 1 | 18 |

Form submission failed

19 |

Your request was missing the required security token. Please try submitting your request again.

20 | -------------------------------------------------------------------------------- /templates/error403.php: -------------------------------------------------------------------------------- 1 | 18 |

Access denied

19 |

Sorry, but you don't have permission to view this page.

20 | -------------------------------------------------------------------------------- /templates/error404.php: -------------------------------------------------------------------------------- 1 | 18 |

Page not found

19 |

Sorry, but the address you've given doesn't seem to point to a valid page.

20 |

If you got here by following a link, please report it to us. Otherwise, please make sure that you have typed the address correctly, or just start browsing from the home page.

21 | -------------------------------------------------------------------------------- /templates/error500.php: -------------------------------------------------------------------------------- 1 | 18 | get('error_details')) { ?> 19 |

Error

20 |

get('exception_class')) ?> "get('error_details')->getMessage()) ?>"

21 |

Occurred in get('error_details')->getFile().' line '.$this->get('error_details')->getLine()) ?>

22 |

Stack trace

23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | get('error_details')->getTrace() as $stack_line) { ?> 33 | 34 | 35 | 36 | 45 | 46 | 47 | 48 | 49 | 50 |
FunctionArgumentsLocation
37 | 38 |
    39 | 40 |
  • 41 | 42 |
43 | 44 |
51 | 52 |

Oops! Something went wrong!

53 |

Sorry, but it looks like something needs fixing on the system. The problem has been automatically reported to the administrators, but if you wish, you can also provide additional information about what you were doing that may have triggered the error.

54 | 55 | -------------------------------------------------------------------------------- /templates/error503.php: -------------------------------------------------------------------------------- 1 | 18 |

System is down for maintenance

19 |

Sorry for the inconvenience. We should be back soon though, so press the reload button in your browser in a few minutes to try again.

20 | -------------------------------------------------------------------------------- /templates/error503_upstream.php: -------------------------------------------------------------------------------- 1 | get('active_user'); 18 | ?> 19 |

DNS server communication failure

20 |

The DNS server is not responding.

21 | admin) { ?> 22 |

Make sure that the following PowerDNS parameters are set correctly in pdns.conf:

23 |
webserver=yes
24 | webserver-address=...
25 | webserver-allow-from=...
26 | webserver-port=...
27 | api=yes
28 | api-key=...
29 |

Reload PowerDNS after making changes to this file.

30 |

Also check the values set in the [powerdns] section of the DNS UI configuration file (config/config.ini).

31 | 32 | -------------------------------------------------------------------------------- /templates/functions.php: -------------------------------------------------------------------------------- 1 | '.hesc($before).' '.hesc($after).'', ESC_NONE); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /templates/settings.php: -------------------------------------------------------------------------------- 1 | get('replication_types'); 18 | $ns_templates = $this->get('ns_templates'); 19 | $soa_templates = $this->get('soa_templates'); 20 | ?> 21 |

Settings

22 |
23 |
24 | Defaults (new zone) 25 |

When creating a new zone, these settings will be used to pre-fill the form.

26 | get('active_user')->get_csrf_field(), ESC_NONE) ?> 27 |
28 | 29 |
30 | 31 |
32 | 36 |
37 | 38 |
39 |
40 |
41 | 42 |
43 | 49 |
50 |
51 |
52 | 53 |
54 | 60 |
61 |
62 |
63 |
64 | 65 |
66 |
67 |
68 |
-------------------------------------------------------------------------------- /templates/template.php: -------------------------------------------------------------------------------- 1 | get('type'); 18 | $template = $this->get('template'); 19 | ?> 20 |

templates: name)?>

21 |
22 | get('active_user')->get_csrf_field(), ESC_NONE) ?> 23 |
24 | 25 |
26 | 27 |
28 |
29 | 30 |
31 | 32 |
33 | 34 |
35 |
36 |
37 | 38 |
39 | 40 |
41 |
42 |
43 | 44 |
45 | 46 |
47 |
48 |
49 | 50 |
51 | 52 |
53 |
54 |
55 | 56 |
57 | 58 |
59 |
60 |
61 | 62 |
63 | 64 |
65 |
66 |
67 | 68 |
69 | 70 |
71 |
72 | 73 |
74 | 75 |
76 | 77 |
78 |
79 | 80 |
81 |
82 | 83 |
84 |
85 |
86 | -------------------------------------------------------------------------------- /templates/templates.php: -------------------------------------------------------------------------------- 1 | get('soa_templates'); 18 | $templates['ns'] = $this->get('ns_templates'); 19 | $type = $this->get('type'); 20 | $types = array('soa', 'ns'); 21 | ?> 22 |

get('title'))?>

23 |

These templates are used when creating new zones to pre-populate the form fields. Any number of preset templates can be defined below. If a default is selected, its values will be pre-filled without user interaction.

24 | 25 | 29 | 30 |
31 |
32 |

Template list

33 |
34 | get('active_user')->get_csrf_field(), ESC_NONE) ?> 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 51 | 52 | 53 | 54 | 55 | 56 | 65 | 66 | 71 | 72 |
TypeTemplate nameActions
name)?> 57 | Edit 58 | 59 | default) { ?> 60 | 61 | 62 | 63 | 64 |
73 |
74 |
75 | 76 |
77 |

Create template

78 |
79 | get('active_user')->get_csrf_field(), ESC_NONE) ?> 80 |
81 | 82 |
83 | 84 |
85 |
86 | 87 |
88 | 89 |
90 | 91 |
92 |
93 |
94 | 95 |
96 | 97 |
98 |
99 |
100 | 101 |
102 | 103 |
104 |
105 |
106 | 107 |
108 | 109 |
110 |
111 |
112 | 113 |
114 | 115 |
116 |
117 |
118 | 119 |
120 | 121 |
122 |
123 |
124 | 125 |
126 | 127 |
128 |
129 | 130 |
131 | 132 |
133 | 134 |
135 |
136 | 137 |
138 |
139 | 140 |
141 |
142 |
143 |
144 | 145 |
146 | -------------------------------------------------------------------------------- /templates/user.php: -------------------------------------------------------------------------------- 1 | get('active_user'); 18 | $user = $this->get('user'); 19 | $changesets = $this->get('changesets'); 20 | global $output_formatter; 21 | ?> 22 |

name)?> (uid)?>)

23 |

User details

24 | admin && $user->auth_realm === 'local') { ?> 25 |
26 | get('active_user')->get_csrf_field(), ESC_NONE) ?> 27 |
28 | 29 |
30 |

uid)?>

31 |
32 |
33 |
34 | 35 |
36 | 37 |
38 |
39 |
40 | 41 |
42 | 43 |
44 |
45 |
46 | 47 |
48 |
49 | 50 |
51 |
52 |
53 |
54 | 55 |
56 |
57 | 58 |
59 |
60 |
61 |
62 |
63 | 64 |
65 |
66 |
67 | 68 |
69 |
User ID
70 |
uid)?>
71 |
Full name
72 |
name)?>
73 |
Email address
74 |
email)?>
75 |
76 | 77 |

Activity

78 | 79 |

No activity yet.

80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |
Date / timeCommentRequesterZoneChanges
change_date->format('Y-m-d H:i:s'))?>changeset_comment_format($changeset->comment), ESC_NONE) ?>requester) { ?>requester->name)?>zone->name)))?>deleted.'/+'.$changeset->added)?>
105 | 106 | -------------------------------------------------------------------------------- /templates/user_not_found.php: -------------------------------------------------------------------------------- 1 | 18 |

User not found

19 |

The specified user get('uid'))?> could not be found.

20 | -------------------------------------------------------------------------------- /templates/users.php: -------------------------------------------------------------------------------- 1 | get('users'); 18 | ?> 19 |

Users

20 | 24 |
25 |
26 |

User list

27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | active) out(' class="text-muted"', ESC_NONE) ?>> 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
User IDFull nameEmail addressDirectoryActiveAdmin
uid)?>name)?>email)?>auth_realm ?? ''))?>active ? '✓' : '')?>admin ? '✓' : '')?>
51 |
52 |
53 |

Create user

54 |

You can create users in the local directory here. It is not possible to create users in your LDAP directory from the DNS UI.

55 |
56 | get('active_user')->get_csrf_field(), ESC_NONE) ?> 57 |
58 | 59 |
60 | 61 |
62 |
63 |
64 | 65 |
66 | 67 |
68 |
69 |
70 | 71 |
72 | 73 |
74 |
75 |
76 | 77 |
78 |
79 | 80 |
81 |
82 |
83 |
84 |
85 | 86 |
87 |
88 |
89 |
90 |
91 | -------------------------------------------------------------------------------- /templates/zone_add_failed.php: -------------------------------------------------------------------------------- 1 | 18 |

Zone creation failed

19 |

The zone creation failed. The following error message was given: get('message'))?>

20 | -------------------------------------------------------------------------------- /templates/zone_update_failed.php: -------------------------------------------------------------------------------- 1 | 18 |

Zone update failed

19 |

The zone update failed. The following error message was given: get('message'))?>

20 | -------------------------------------------------------------------------------- /templates/zonedeleted.php: -------------------------------------------------------------------------------- 1 | get('active_user'); 18 | $zone = $this->get('zone'); 19 | $deletion = $this->get('deletion'); 20 | ?> 21 |

Zone name)))?> does not exist

22 |

This zone no longer exists on the DNS server.

23 | 24 |
25 |
Deletion requested by
26 |
name)?> on format('Y-m-d H:i:s'))?>
27 |
Deletion confirmed by
28 |
name)?> on format('Y-m-d H:i:s'))?>
29 |
30 |

Zone archive

31 |

This is a snapshot of the zone's contents prior to its deletion.

32 |
33 |
34 | get('active_user')->get_csrf_field(), ESC_NONE) ?> 35 |
36 | 37 |
38 | 39 | -------------------------------------------------------------------------------- /templates/zoneexport.php: -------------------------------------------------------------------------------- 1 | get('zone'); 18 | $rrsets = $this->get('rrsets'); 19 | echo $zone->export_as_bind9_format(); 20 | -------------------------------------------------------------------------------- /templates/zoneimport.php: -------------------------------------------------------------------------------- 1 | get('zone'); 18 | $modifications = $this->get('modifications'); 19 | $checked = 'checked '; 20 | $count = 0; 21 | $limit = 2500; 22 | ?> 23 |

Import preview for name)))?> zone update

24 | 25 |

No changes have been made! Go back.

26 | 27 |
28 | get('active_user')->get_csrf_field(), ESC_NONE) ?> 29 | 0) { ?> 30 |

New resource recordsets

31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | $limit) $checked = ''; 47 | ?> 48 | 49 | 50 | 51 | 52 | 59 | 66 | 67 | 68 | 69 | 70 |
NameTypeTTLDataCommentsOkay to add?
name, $zone->name))?>type)?>ttl))?> 53 |
    54 | list_resource_records() as $rr) { ?> 55 |
  • content.', Enabled: '.($rr->disabled ? 'No' : 'Yes')) ?>
  • 56 | 57 |
58 |
60 |
    61 | list_comments() as $comment) { ?> 62 |
  • content) ?>
  • 63 | 64 |
65 |
value="">
71 | 72 | 0) { ?> 73 |

Updated resource recordsets

74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | $limit) $checked = ''; 89 | ?> 90 | 91 | 92 | 93 | 94 | 101 | 102 | 103 | 104 | 105 |
NameTypeTTLChangesOkay to update?
name, $zone->name))?>type)?>ttl))?> 95 |
    96 | 97 |
  • 98 | 99 |
100 |
value="">
106 | 107 | 0) { ?> 108 |

Deleted resource recordsets

109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | $limit) $checked = ''; 125 | ?> 126 | 127 | 128 | 129 | 130 | 137 | 144 | 145 | 146 | 147 | 148 |
NameTypeTTLDataCommentsOkay to delete?
name, $zone->name))?>type)?>ttl))?> 131 |
    132 | list_resource_records() as $rr) { ?> 133 |
  • content) ?>
  • 134 | 135 |
136 |
138 |
    139 | list_comments() as $comment) { ?> 140 |
  • content) ?>
  • 141 | 142 |
143 |
value="">
149 | 150 | 151 |

152 | By default only the first changes (out of ) have been selected for import as larger imports may be rejected by PowerDNS. 153 | It is recommended that you run this import multiple times until all changes have been imported. 154 |

155 | 156 |
157 |

158 | 159 | Cancel import 160 |

161 |
162 | 163 | -------------------------------------------------------------------------------- /templates/zonesplit.php: -------------------------------------------------------------------------------- 1 | get('zone'); 19 | $newzonename = $this->get('newzonename'); 20 | $suffix = $this->get('suffix'); 21 | $split = $this->get('split'); 22 | $cname_error = $this->get('cname_error'); 23 | ?> 24 |

Zone split of from name)))?>

25 |
26 | get('active_user')->get_csrf_field(), ESC_NONE) ?> 27 | 28 |

No records match this pattern.

29 |

Go back

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | type == 'NS' && $rrset->name == $newzonename) continue; 47 | $rrsetnum++; 48 | $rrs = $rrset->list_resource_records(); 49 | $name = DNSName::abbreviate($rrset->name, $zone->name); 50 | $newname = DNSName::abbreviate($rrset->name, $newzonename); 51 | $rowclasses = array(); 52 | $firstrow = reset($rrs); 53 | if($firstrow->disabled) $rowclasses[] = 'disabled'; 54 | if($newname == '@' && $rrset->type == 'CNAME') $rowclasses[] = 'danger'; 55 | ?> 56 | 57 | 58 | 59 | 60 | 61 | disabled) $rowclasses[] = 'disabled'; 66 | $count++; 67 | if($count > 1) { 68 | out(' 0) { 70 | out(' class="'.hesc(implode(' ', $rowclasses)).'"', ESC_NONE); 71 | } 72 | out('>', ESC_NONE); 73 | } 74 | $rr->content = DNSContent::decode($rr->content, $rrset->type, $zone->name); 75 | ?> 76 | 77 | 78 | 81 | 82 | 83 | 84 | 85 | 86 |
87 | 88 | 89 |
90 | 91 |
92 | 93 | 94 | 95 | 96 | Cancel 97 |
98 | 99 |
100 | -------------------------------------------------------------------------------- /templates/zonesplitcompleted.php: -------------------------------------------------------------------------------- 1 | get('zone'); 19 | $newzonename = $this->get('newzonename'); 20 | ?> 21 |

Zone split of from name)))?>

22 | 26 | -------------------------------------------------------------------------------- /tests/DNSContentTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('ns1.example.com. hostmaster.example.com. 2017080806 10800 3600 1209600 3600', DNSContent::encode('ns1.example.com. hostmaster.example.com. 2017080806 3H 1H 2W 1H', 'SOA', 'example.com.')); 14 | } 15 | public function testEncodeTxt() { 16 | // TXT should be escaped and quoted 17 | $this->assertEquals('"hello \"world\" \\\\"', DNSContent::encode('hello "world" \\', 'TXT', 'example.com.')); 18 | } 19 | public function testEncodeMx() { 20 | // MX hostname should be canonified 21 | $this->assertEquals('10 example.com.', DNSContent::encode('10 @', 'MX', 'example.com.')); 22 | $this->assertEquals('10 test.example.com.', DNSContent::encode('10 test', 'MX', 'example.com.')); 23 | $this->assertEquals('10 test.example.org.', DNSContent::encode('10 test.example.org.', 'MX', 'example.com.')); 24 | } 25 | public function testEncodeSrv() { 26 | // SRV hostname should be canonified 27 | $this->assertEquals('0 5 5060 example.com.', DNSContent::encode('0 5 5060 @', 'SRV', 'example.com.')); 28 | $this->assertEquals('0 5 5060 sipserver.example.com.', DNSContent::encode('0 5 5060 sipserver', 'SRV', 'example.com.')); 29 | $this->assertEquals('0 5 5060 sipserver.example.org.', DNSContent::encode('0 5 5060 sipserver.example.org.', 'SRV', 'example.com.')); 30 | } 31 | public function testEncodeCname() { 32 | // CNAME should be canonified 33 | $this->assertEquals('example.com.', DNSContent::encode('@', 'CNAME', 'example.com.')); 34 | $this->assertEquals('test.example.com.', DNSContent::encode('test', 'CNAME', 'example.com.')); 35 | $this->assertEquals('test.example.org.', DNSContent::encode('test.example.org.', 'CNAME', 'example.com.')); 36 | } 37 | public function testEncodeA() { 38 | // A records should be untouched 39 | $this->assertEquals('192.0.2.1', DNSContent::encode('192.0.2.1', 'A', 'example.com.')); 40 | } 41 | 42 | public function testDecodeTxt() { 43 | // TXT should be unquoted and unescaped 44 | $this->assertEquals('hello "world" \\', DNSContent::decode('"hello \"world\" \\\\"', 'TXT', 'example.com.')); 45 | } 46 | public function testDecodeMx() { 47 | // MX hostname should be abbreviated 48 | $this->assertEquals('10 @', DNSContent::decode('10 example.com.', 'MX', 'example.com.')); 49 | $this->assertEquals('10 test', DNSContent::decode('10 test.example.com.', 'MX', 'example.com.')); 50 | $this->assertEquals('10 test.example.org.', DNSContent::decode('10 test.example.org.', 'MX', 'example.com.')); 51 | } 52 | public function testDecodeSrv() { 53 | // SRV hostname should be abbreviated 54 | $this->assertEquals('0 5 5060 @', DNSContent::decode('0 5 5060 example.com.', 'SRV', 'example.com.')); 55 | $this->assertEquals('0 5 5060 sipserver', DNSContent::decode('0 5 5060 sipserver.example.com.', 'SRV', 'example.com.')); 56 | $this->assertEquals('0 5 5060 sipserver.example.org.', DNSContent::decode('0 5 5060 sipserver.example.org.', 'SRV', 'example.com.')); 57 | } 58 | public function testDecodeCname() { 59 | // CNAME should be abbreviated 60 | $this->assertEquals('@', DNSContent::decode('example.com.', 'CNAME', 'example.com.')); 61 | $this->assertEquals('test', DNSContent::decode('test.example.com.', 'CNAME', 'example.com.')); 62 | $this->assertEquals('test.example.org.', DNSContent::decode('test.example.org.', 'CNAME', 'example.com.')); 63 | } 64 | public function testDecodeA() { 65 | // A records should be untouched 66 | $this->assertEquals('192.0.2.1', DNSContent::decode('192.0.2.1', 'A', 'example.com.')); 67 | } 68 | 69 | public function testBind9FormatSoa() { 70 | // SOA record should be formatted nicely 71 | $nice_format = "ns1 hostmaster.example.com. (\n"; 72 | $nice_format .= " 2017080806 ; serial\n"; 73 | $nice_format .= " 3H ; refresh\n"; 74 | $nice_format .= " 1H ; retry\n"; 75 | $nice_format .= " 2W ; expire\n"; 76 | $nice_format .= " 1H ; default ttl\n"; 77 | $nice_format .= " )\n"; 78 | $this->assertEquals($nice_format, DNSContent::bind9_format('ns1.example.com. hostmaster.example.com. 2017080806 10800 3600 1209600 3600', 'SOA', 'example.com.')); 79 | } 80 | public function testBind9FormatTxt() { 81 | // TXT record encoding should be untouched (should already be encoded) 82 | $this->assertEquals('"hello \"world\" \\\\"', DNSContent::bind9_format('"hello \"world\" \\\\"', 'TXT', 'example.com.')); 83 | // TXT records should be split at 255 bytes 84 | $this->assertEquals( 85 | '"0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'. 86 | '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'. 87 | '0123456789012345678901234567890123456789012345678901234" "567890123456789012345678901234567890123456789"', 88 | DNSContent::bind9_format('"'.str_repeat("0123456789", 30).'"', 'TXT', 'example.com.') 89 | ); 90 | $this->assertEquals( 91 | '"\\\\123456789\\\\123456789\\\\123456789\\\\123456789\\\\123456789\\\\123456789\\\\123456789\\\\123456789\\\\123456789\\\\123456789'. 92 | '\\\\123456789\\\\123456789\\\\123456789\\\\123456789\\\\123456789\\\\123456789\\\\123456789\\\\123456789\\\\123456789\\\\123456789'. 93 | '\\\\123456789\\\\123456789\\\\123456789\\\\123456789\\\\123456789\\\\1234" "56789\\\\123456789\\\\123456789\\\\123456789\\\\123456789"', 94 | DNSContent::bind9_format('"'.str_repeat("\\123456789", 30).'"', 'TXT', 'example.com.') 95 | ); 96 | } 97 | public function testBind9FormatA() { 98 | // A records should be untouched 99 | $this->assertEquals('192.0.2.1', DNSContent::bind9_format('192.0.2.1', 'A', 'example.com.')); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/DNSKEYTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('50036', DNSKEY::get_tag(257, 3, 13, '+lIB+O45g/Uea2u5v8mhWaW9pi4CaKEKiPK3AbYH5Uja9GW7+m/vUOBCHwICf3hLtZ5PXgorjP/td9qutBneFw==')); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/DNSNameTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('foobar', DNSName::abbreviate('foobar.example.com.', 'example.com.')); 14 | // Non-matching domain 15 | $this->assertEquals('foobar.example.com.', DNSName::abbreviate('foobar.example.com.', 'example.org.')); 16 | // Non-matching domain (partial match) 17 | $this->assertEquals('foobar.example.com.', DNSName::abbreviate('foobar.example.com.', 'test.com.')); 18 | // Non-matching domain (substring match) 19 | $this->assertEquals('foobar.example.com.', DNSName::abbreviate('foobar.example.com.', 'bar.example.com.')); 20 | // Domain root 21 | $this->assertEquals('@', DNSName::abbreviate('foobar.example.com.', 'foobar.example.com.')); 22 | } 23 | 24 | public function testCanonify() { 25 | // Normal record 26 | $this->assertEquals('foobar.example.com.', DNSName::canonify('foobar', 'example.com.')); 27 | // Dot-qualified FQDN 28 | $this->assertEquals('foobar.example.org.', DNSName::canonify('foobar.example.org.', 'example.com.')); 29 | // Not dot-qualified FQDN 30 | $this->assertEquals('foobar.example.org.example.com.', DNSName::canonify('foobar.example.org', 'example.com.')); 31 | // Domain root record 32 | $this->assertEquals('foobar.example.com.', DNSName::canonify('@', 'foobar.example.com.')); 33 | // . origin 34 | $this->assertEquals('foobar.example.com.', DNSName::canonify('foobar.example.com', '.')); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/DNSTimeTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('0', DNSTime::abbreviate(0)); 18 | // Seconds 19 | $this->assertEquals('1', DNSTime::abbreviate(1)); 20 | $this->assertEquals('59', DNSTime::abbreviate(59)); 21 | // Minutes 22 | $this->assertEquals('1M', DNSTime::abbreviate(1 * $minute)); 23 | $this->assertEquals('59M', DNSTime::abbreviate(59 * $minute)); 24 | // Combined minutes + seconds 25 | $this->assertEquals('61', DNSTime::abbreviate(1 * $minute + 1)); 26 | // Hours 27 | $this->assertEquals('1H', DNSTime::abbreviate(1 * $hour)); 28 | $this->assertEquals('23H', DNSTime::abbreviate(23 * $hour)); 29 | // Combined hours + minutes 30 | $this->assertEquals('61M', DNSTime::abbreviate(1 * $hour + 1 * $minute)); 31 | // Combined hours + minutes + seconds 32 | $this->assertEquals('3661', DNSTime::abbreviate(1 * $hour + 1 * $minute + 1)); 33 | // Days 34 | $this->assertEquals('1D', DNSTime::abbreviate(1 * $day)); 35 | $this->assertEquals('6D', DNSTime::abbreviate(6 * $day)); 36 | // Weeks 37 | $this->assertEquals('1W', DNSTime::abbreviate(1 * $week)); 38 | $this->assertEquals('99W', DNSTime::abbreviate(99 * $week)); 39 | } 40 | 41 | public function testExpand() { 42 | $minute = 60; 43 | $hour = 60 * $minute; 44 | $day = 24 * $hour; 45 | $week = 7 * $day; 46 | // Zero 47 | $this->assertEquals(0, DNSTime::expand('0')); 48 | // Seconds 49 | $this->assertEquals(1, DNSTime::expand('1')); 50 | $this->assertEquals(59, DNSTime::expand('59')); 51 | // Minutes 52 | $this->assertEquals(1 * $minute, DNSTime::expand('1M')); 53 | $this->assertEquals(59 * $minute, DNSTime::expand('59M')); 54 | // Combined minutes + seconds 55 | $this->assertEquals(1 * $minute + 1, DNSTime::expand('1M1S')); 56 | // Hours 57 | $this->assertEquals(1 * $hour, DNSTime::expand('1H')); 58 | $this->assertEquals(23 * $hour, DNSTime::expand('23H')); 59 | // Combined hours + minutes 60 | $this->assertEquals(1 * $hour + 1 * $minute, DNSTime::expand('1H1M')); 61 | // Combined hours + minutes + seconds 62 | $this->assertEquals(1 * $hour + 1 * $minute + 1, DNSTime::expand('1H1M1S')); 63 | // Days 64 | $this->assertEquals(1 * $day, DNSTime::expand('1D')); 65 | $this->assertEquals(6 * $day, DNSTime::expand('6D')); 66 | // Weeks 67 | $this->assertEquals(1 * $week, DNSTime::expand('1W')); 68 | $this->assertEquals(99 * $week, DNSTime::expand('99W')); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /vagrant/README.md: -------------------------------------------------------------------------------- 1 | Opera DNS UI Vagrant 2 | ==================== 3 | 4 | Vagrant and ansible configuration to get a local development or test environment 5 | up and running quickly and efficiently. 6 | 7 | Requirements 8 | ------------ 9 | * Vagrant 2+ 10 | 11 | Usage 12 | ----- 13 | 1. Check out this repository 14 | 2. Enter the 'vagrant' directory 15 | 3. Run `vagrant up` 16 | 4. By default vagrant will map the webserver to port 8000, and the DNS server to 17 | port 5300, but it will pick another port when it is in use. Look for: 18 | ``` 19 | ==> default: Forwarding ports... 20 | default: 80 (guest) => 8000 (host) (adapter 1) 21 | default: 53 (guest) => 5300 (host) (adapter 1) 22 | ``` 23 | 5. Browse to http://localhost:8000 (or the port found in step 4) 24 | 6. Dig using `dig @127.0.0.1 -p 5300` (or the port found in step 4) 25 | 26 | Updating 27 | -------- 28 | If you made changes to the ansible scripts or settings and wish to roll out 29 | again, do the following: 30 | 1. Remove your config/config.ini DNS UI configuration (ansible will recreate it) 31 | 2. Run `vagrant provision` 32 | 33 | If you made a lot of impacting changes which might interfere with the existing 34 | vagrant box, just toss & recreate it: 35 | ``` 36 | vagrant destroy -f 37 | vagrant up 38 | ``` 39 | -------------------------------------------------------------------------------- /vagrant/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "ubuntu/xenial64" 6 | 7 | # Expose TCP/80 and UDP/53 on localhost TCP/8080 and UDP/5353 respectively 8 | # If the ports are in use, Vagrant will pick another and let the user know 9 | config.vm.network "forwarded_port", protocol: "tcp", guest: 80, host: 8000, 10 | host_ip: "127.0.0.1", auto_correct: true 11 | config.vm.network "forwarded_port", protocol: "udp", guest: 53, host: 5300, 12 | host_ip: "127.0.0.1", auto_correct: true 13 | 14 | config.vm.synced_folder "..", "/vagrant", id: "application" 15 | 16 | config.vm.provision "ansible_local" do |ansible| 17 | ansible.compatibility_mode = "2.0" 18 | ansible.provisioning_path = "/vagrant/vagrant/ansible" 19 | ansible.playbook = "dns-ui.yml" 20 | ansible.verbose = false 21 | ansible.install = true 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /vagrant/ansible/config.sample.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ## Rename this file to 'config.yml' to activate, and remove the comment from 3 | ## any setting you wish to change from the default value. Defaults are set in 4 | ## roles/*/defaults/main.yml. 5 | 6 | ## Database to install (currently supported: postgresql) 7 | # database_vendor: postgresql 8 | 9 | ## Database settings for DNS UI database 10 | # database_dnsui_schema: dns-ui 11 | # database_dnsui_username: dns-ui 12 | # database_dnsui_password: dns-ui 13 | 14 | ## Database settings for PowerDNS database 15 | # database_powerdns_schema: powerdns 16 | # database_powerdns_username: powerdns 17 | # database_powerdns_password: powerdns 18 | 19 | ## Power DNS repository information 20 | # powerdns_repo_key: https://repo.powerdns.com/FD380FBB-pub.asc 21 | # powerdns_repo: deb [arch=amd64] http://repo.powerdns.com/ubuntu xenial-auth-41 main 22 | 23 | ## PowerDNS default configuration settings 24 | # powerdns_default_soa_name: ns1.dns-ui.local 25 | # powerdns_default_soa_mail: hostmaster.dns-ui.local 26 | ## The default-ksk-algorithm name depends on the PowerDNS version 27 | ## For Version 4.0 use default-ksk-algorithms 28 | ## For Version 4.1 use default-ksk-algorithm 29 | # powerdns_default_ksk_algorithm_setting: default-ksk-algorithm 30 | # powerdns_default_ksk_algorithm: ecdsa256 31 | # powerdns_api_key: pdns-api-key 32 | 33 | ## LDAP settings 34 | # ldap_domain: dns-ui.local 35 | # ldap_admin_password: dnsui 36 | # ldap_organisation: DNS UI 37 | # dnsui_auth_name: DNS UI 38 | 39 | ## DNS UI users 40 | # dnsui_admin_username: admin 41 | # dnsui_admin_password: admin 42 | # dnsui_user_username: user 43 | # dnsui_user_password: user 44 | -------------------------------------------------------------------------------- /vagrant/ansible/dns-ui.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | become_method: sudo 4 | become: true 5 | gather_facts: false 6 | roles: 7 | - dns-ui 8 | -------------------------------------------------------------------------------- /vagrant/ansible/roles/dns-ui/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | database_vendor: postgresql 3 | database_dnsui_schema: dns-ui 4 | database_dnsui_username: dns-ui 5 | database_dnsui_password: dns-ui 6 | database_powerdns_schema: powerdns 7 | database_powerdns_username: powerdns 8 | database_powerdns_password: powerdns 9 | powerdns_repo_key: https://repo.powerdns.com/FD380FBB-pub.asc 10 | powerdns_repo: deb [arch=amd64] http://repo.powerdns.com/ubuntu xenial-auth-41 main 11 | powerdns_default_soa_name: ns1.dns-ui.local 12 | powerdns_default_soa_mail: hostmaster.dns-ui.local 13 | powerdns_default_ksk_algorithm_setting: default-ksk-algorithm 14 | powerdns_default_ksk_algorithm: ecdsa256 15 | powerdns_api_key: pdns-api-key 16 | ldap_domain: dns-ui.local 17 | ldap_admin_password: dnsui 18 | ldap_organisation: DNS UI 19 | dnsui_auth_name: DNS UI 20 | dnsui_admin_username: admin 21 | dnsui_admin_password: admin 22 | dnsui_user_username: user 23 | dnsui_user_password: user 24 | -------------------------------------------------------------------------------- /vagrant/ansible/roles/dns-ui/tasks/apache2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: enable apache ldap module 3 | apache2_module: 4 | name: authnz_ldap 5 | state: present 6 | register: apache_module 7 | 8 | - name: disable default apache configuration 9 | file: 10 | path: /etc/apache2/sites-enabled/000-default.conf 11 | state: absent 12 | 13 | - name: create dns-ui apache configuration 14 | template: 15 | src: apache2.conf.j2 16 | dest: /etc/apache2/sites-available/dns-ui.conf 17 | owner: root 18 | group: root 19 | mode: 0644 20 | register: apache_config_uploaded 21 | 22 | - name: enable dns-ui apache configuration 23 | file: 24 | src: /etc/apache2/sites-available/dns-ui.conf 25 | dest: /etc/apache2/sites-enabled/dns-ui.conf 26 | state: link 27 | register: apache_config_linked 28 | 29 | - name: restart apache 30 | service: 31 | name: apache2 32 | state: restarted 33 | when: apache_module.changed or apache_config_uploaded.changed or apache_config_linked.changed 34 | -------------------------------------------------------------------------------- /vagrant/ansible/roles/dns-ui/tasks/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: check existence of config file 3 | stat: 4 | path: /vagrant/vagrant/ansible/config.yml 5 | register: override_config 6 | 7 | - name: load override variables 8 | include_vars: /vagrant/vagrant/ansible/config.yml 9 | when: override_config.stat.exists 10 | 11 | - name: generate ldap domain variable 12 | set_fact: 13 | ldap_domain_dc: "dc={{ ldap_domain | regex_replace('\\.', ',dc=') }}" 14 | 15 | - name: test if database vendor is supported 16 | fail: 17 | msg: "Database vendor '{{ database_vendor }}' is (currently) not supported" 18 | when: "database_vendor not in supported_db_vendors" 19 | 20 | - set_fact: 21 | ansible_database_package: python-psycopg2 22 | database_package: postgresql 23 | powerdns_database_package: pdns-backend-pgsql 24 | powerdns_database_config: pdns.gpgsql.conf 25 | php_database_package: php-pgsql 26 | when: "database_vendor == 'postgresql'" 27 | -------------------------------------------------------------------------------- /vagrant/ansible/roles/dns-ui/tasks/dns-ui.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: create dns-ui config.ini 3 | copy: 4 | src: /vagrant/config/config-sample.ini 5 | dest: /vagrant/config/config.ini 6 | remote_src: yes 7 | force: no 8 | register: new_config 9 | 10 | - name: configure config.ini 11 | ini_file: 12 | path: /vagrant/config/config.ini 13 | section: "{{ item.section }}" 14 | option: "{{ item.option }}" 15 | value: "{{ item.value }}" 16 | backup: no 17 | when: new_config.changed 18 | loop: 19 | - { section: "web", option: "baseurl", value: "http://localhost:8080" } 20 | - { section: "database", option: "dsn", value: "\"pgsql:host=127.0.0.1;dbname={{ database_dnsui_schema }}\"" } 21 | - { section: "database", option: "username", value: "{{ database_dnsui_username }}" } 22 | - { section: "database", option: "password", value: "{{ database_dnsui_password }}" } 23 | - { section: "ldap", option: "host", value: "ldap://localhost:389" } 24 | - { section: "ldap", option: "starttls", value: "0" } 25 | - { section: "ldap", option: "dn_user", value: "\"ou=users,{{ ldap_domain_dc }}\"" } 26 | - { section: "ldap", option: "dn_group", value: "\"ou=groups,{{ ldap_domain_dc }}\"" } 27 | - { section: "ldap", option: "bind_dn", value: "\"cn=admin,{{ ldap_domain_dc }}\"" } 28 | - { section: "ldap", option: "bind_password", value: "{{ ldap_admin_password }}" } 29 | - { section: "ldap", option: "admin_group_cn", value: "dnsui_admins" } 30 | - { section: "ldap", option: "user_email", value: "cn" } 31 | - { section: "powerdns", option: "api_url", value: "\"http://localhost:8081/api/v1/servers/localhost\"" } 32 | - { section: "powerdns", option: "api_key", value: "{{ powerdns_api_key }}" } 33 | -------------------------------------------------------------------------------- /vagrant/ansible/roles/dns-ui/tasks/ldap.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: generate ldap password for admin 3 | command: slappasswd -s {{ dnsui_admin_password }} 4 | register: cn_admin_password 5 | 6 | - name: generate ldap password for user 7 | command: slappasswd -s {{ dnsui_user_password }} 8 | register: cn_user_password 9 | 10 | - name: create ldap ou's 11 | ldap_entry: 12 | dn: "{{ item }}" 13 | objectClass: organizationalUnit 14 | server_uri: ldap://localhost/ 15 | bind_dn: cn=admin,{{ ldap_domain_dc }} 16 | bind_pw: "{{ ldap_admin_password }}" 17 | loop: 18 | - ou=users,{{ ldap_domain_dc }} 19 | - ou=groups,{{ ldap_domain_dc }} 20 | 21 | - name: create ldap users 22 | ldap_entry: 23 | dn: "uid={{ item.user }},ou=users,{{ ldap_domain_dc }}" 24 | objectClass: 25 | - top 26 | - account 27 | - posixAccount 28 | - shadowAccount 29 | attributes: 30 | cn: "{{ item.user }}" 31 | userPassword: "{{ item.password }}" 32 | uidNumber: 1000 33 | gidNumber: 1000 34 | homeDirectory: /tmp 35 | loginShell: /usr/sbin/nologin 36 | shadowLastChange: 0 37 | shadowMin: 0 38 | shadowMax: 99999 39 | shadowWarning: 7 40 | server_uri: ldap://localhost/ 41 | bind_dn: cn=admin,{{ ldap_domain_dc }} 42 | bind_pw: "{{ ldap_admin_password }}" 43 | loop: 44 | - { user: "{{ dnsui_admin_username }}", password: "{{ cn_admin_password.stdout }}" } # password: admin 45 | - { user: "{{ dnsui_user_username }}", password: "{{ cn_user_password.stdout }}" } # password: user 46 | 47 | - name: create ldap dnsui_admins posix group 48 | ldap_entry: 49 | dn: cn=dnsui_admins,ou=groups,{{ ldap_domain_dc }} 50 | objectClass: 51 | - posixGroup 52 | - top 53 | attributes: 54 | description: dnsui_admins 55 | gidNumber: 100 56 | memberUid: "{{ dnsui_admin_username }}" 57 | server_uri: ldap://localhost/ 58 | bind_dn: cn=admin,{{ ldap_domain_dc }} 59 | bind_pw: "{{ ldap_admin_password }}" 60 | -------------------------------------------------------------------------------- /vagrant/ansible/roles/dns-ui/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - import_tasks: ping.yml 3 | tags: ping 4 | - import_tasks: config.yml 5 | tags: config 6 | - import_tasks: repo.yml 7 | tags: repo 8 | - import_tasks: packages.yml 9 | tags: packages 10 | - import_tasks: ldap.yml 11 | tags: ldap 12 | - import_tasks: postgresql.yml 13 | tags: postgresql 14 | - import_tasks: powerdns.yml 15 | tags: powerdns 16 | - import_tasks: apache2.yml 17 | tags: apache2 18 | - import_tasks: dns-ui.yml 19 | tags: dns-ui 20 | -------------------------------------------------------------------------------- /vagrant/ansible/roles/dns-ui/tasks/packages.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: configure slapd package 3 | debconf: 4 | name: slapd 5 | question: "{{ item.question }}" 6 | vtype: "{{ item.vtype }}" 7 | value: "{{ item.value }}" 8 | loop: 9 | - { question: 'slapd/password1', vtype: 'password', value: '{{ ldap_admin_password }}' } 10 | - { question: 'slapd/password2', vtype: 'password', value: '{{ ldap_admin_password }}' } 11 | - { question: 'slapd/backend', vtype: 'select', value: 'MDB' } 12 | - { question: 'slapd/allow_ldap_v2', vtype: 'boolean', value: 'false' } 13 | - { question: 'shared/organization', vtype: 'string', value: '{{ ldap_organisation }}' } 14 | - { question: 'slapd/move_old_database', vtype: 'boolean', value: 'true' } 15 | - { question: 'slapd/domain', vtype: 'string', value: '{{ ldap_domain }}' } 16 | 17 | - name: install required packages 18 | apt: 19 | name: "{{ item }}" 20 | state: present 21 | update_cache: yes 22 | loop: 23 | - python-ldap 24 | - "{{ ansible_database_package }}" 25 | - apache2 26 | - libapache2-mod-php 27 | - php-intl 28 | - php-json 29 | - php-ldap 30 | - "{{ php_database_package }}" 31 | - php-mbstring 32 | - php-curl 33 | - "{{ database_package }}" 34 | - pdns-server 35 | - pdns-tools 36 | - "{{ powerdns_database_package }}" 37 | - slapd 38 | - ldap-utils 39 | -------------------------------------------------------------------------------- /vagrant/ansible/roles/dns-ui/tasks/ping.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: test connection 3 | ping: 4 | -------------------------------------------------------------------------------- /vagrant/ansible/roles/dns-ui/tasks/postgresql.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: create postgresql database '{{ database_dnsui_schema }}' 3 | postgresql_db: 4 | name: "{{ database_dnsui_schema }}" 5 | become: true 6 | become_user: postgres 7 | 8 | - name: create postgresql user '{{ database_dnsui_username }}' 9 | postgresql_user: 10 | db: "{{ database_dnsui_schema }}" 11 | name: "{{ database_dnsui_username }}" 12 | password: "{{ database_dnsui_password }}" 13 | become: true 14 | become_user: postgres 15 | 16 | - name: create postgresql database '{{ database_powerdns_schema }}' 17 | postgresql_db: 18 | name: "{{ database_powerdns_schema }}" 19 | become: true 20 | become_user: postgres 21 | 22 | - name: load schema for database '{{ database_powerdns_schema }}' 23 | postgresql_db: 24 | name: "{{ database_powerdns_schema }}" 25 | state: restore 26 | target: /usr/share/doc/pdns-backend-pgsql/schema.pgsql.sql 27 | become: true 28 | become_user: postgres 29 | 30 | - name: create postgresql user '{{ database_powerdns_username }}' 31 | postgresql_user: 32 | db: "{{ database_powerdns_schema }}" 33 | name: "{{ database_powerdns_username }}" 34 | password: "{{ database_powerdns_password }}" 35 | become: true 36 | become_user: postgres 37 | 38 | - name: grant all privileges for user '{{ database_powerdns_username }}' 39 | postgresql_privs: 40 | db: "{{ database_powerdns_schema }}" 41 | privs: ALL 42 | type: "{{ item }}" 43 | objs: ALL_IN_SCHEMA 44 | roles: "{{ database_powerdns_username }}" 45 | become: true 46 | become_user: postgres 47 | loop: 48 | - table 49 | - sequence 50 | -------------------------------------------------------------------------------- /vagrant/ansible/roles/dns-ui/tasks/powerdns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: remove unused powerdns configuration 3 | file: 4 | name: "{{ item }}" 5 | state: absent 6 | loop: 7 | - /etc/powerdns/bindbackend.conf 8 | - /etc/powerdns/pdns.d/pdns.simplebind.conf 9 | register: pdns_config_clean 10 | 11 | - name: create powerdns database configuration 12 | template: 13 | src: "{{ powerdns_database_config }}.j2" 14 | dest: /etc/powerdns/pdns.d/{{ powerdns_database_config }} 15 | owner: root 16 | group: root 17 | mode: 0644 18 | register: pdns_db_config 19 | 20 | - name: configure powerdns 21 | lineinfile: 22 | path: /etc/powerdns/pdns.conf 23 | regexp: "^{{ item.name }}=" 24 | line: "{{ item.name }}={{ item.value }}" 25 | loop: 26 | - { name: "default-soa-name", value: "{{ powerdns_default_soa_name }}" } 27 | - { name: "default-soa-mail", value: "{{ powerdns_default_soa_mail }}" } 28 | - { name: "{{ powerdns_default_ksk_algorithm_setting }}", value: "{{ powerdns_default_ksk_algorithm }}" } 29 | - { name: "webserver", value: "yes" } 30 | - { name: "webserver-address", value: "0.0.0.0" } 31 | - { name: "webserver-allow-from", value: "127.0.0.1" } 32 | - { name: "webserver-port", value: "8081" } 33 | - { name: "api", value: "yes" } 34 | - { name: "api-key", value: "{{ powerdns_api_key }}" } 35 | register: pdns_config 36 | 37 | - name: restart powerdns 38 | service: 39 | name: pdns 40 | state: restarted 41 | when: pdns_config_clean.changed or pdns_db_config.changed or pdns_config.changed 42 | -------------------------------------------------------------------------------- /vagrant/ansible/roles/dns-ui/tasks/repo.yml: -------------------------------------------------------------------------------- 1 | - name: add powerdns repository key 2 | apt_key: 3 | url: "{{ powerdns_repo_key }}" 4 | state: present 5 | 6 | - name: add powerdns repository 7 | apt_repository: 8 | repo: "{{ powerdns_repo }}" 9 | state: present 10 | filename: powerdns 11 | -------------------------------------------------------------------------------- /vagrant/ansible/roles/dns-ui/templates/apache2.conf.j2: -------------------------------------------------------------------------------- 1 | 2 | DocumentRoot /vagrant/public_html 3 | 4 | CustomLog /var/log/apache2/dns-ui-access.log combined 5 | ErrorLog /var/log/apache2/dns-ui-error.log 6 | 7 | DirectoryIndex init.php 8 | FallbackResource /init.php 9 | AllowEncodedSlashes NoDecode 10 | 11 | 12 | AuthType Basic 13 | AuthName "{{ dnsui_auth_name }}" 14 | AuthBasicProvider ldap 15 | 16 | AuthLDAPURL "ldap://localhost:389/ou=users,{{ ldap_domain_dc }}?uid?sub?(objectClass=*)" 17 | Require valid-user 18 | 19 | 20 | -------------------------------------------------------------------------------- /vagrant/ansible/roles/dns-ui/templates/pdns.gpgsql.conf.j2: -------------------------------------------------------------------------------- 1 | launch=gpgsql 2 | gpgsql-host=127.0.0.1 3 | gpgsql-user={{ database_powerdns_username }} 4 | gpgsql-dbname={{ database_powerdns_schema }} 5 | gpgsql-password={{ database_powerdns_password }} 6 | gpgsql-dnssec=yes 7 | -------------------------------------------------------------------------------- /vagrant/ansible/roles/dns-ui/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | supported_db_vendors: 3 | - postgresql 4 | -------------------------------------------------------------------------------- /views/csrf.php: -------------------------------------------------------------------------------- 1 | set('address', $relative_request_url); 20 | $content->set('fulladdress', $absolute_request_url); 21 | 22 | $page = new PageSection('base'); 23 | $page->set('title', 'Form submission failed'); 24 | $page->set('content', $content); 25 | $page->set('alerts', array()); 26 | echo $page->generate(); 27 | -------------------------------------------------------------------------------- /views/error403.php: -------------------------------------------------------------------------------- 1 | set('address', $relative_request_url); 20 | $content->set('fulladdress', $absolute_request_url); 21 | 22 | $page = new PageSection('base'); 23 | $page->set('title', 'Access denied'); 24 | $page->set('content', $content); 25 | $page->set('alerts', array()); 26 | header('HTTP/1.1 403 Forbidden'); 27 | echo $page->generate(); 28 | -------------------------------------------------------------------------------- /views/error404.php: -------------------------------------------------------------------------------- 1 | set('address', $relative_request_url); 20 | $content->set('fulladdress', $absolute_request_url); 21 | $content->set('referrer', isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''); 22 | 23 | $page = new PageSection('base'); 24 | $page->set('title', 'Page not found'); 25 | $page->set('content', $content); 26 | $page->set('alerts', array()); 27 | header('HTTP/1.1 404 Not Found'); 28 | echo $page->generate(); 29 | -------------------------------------------------------------------------------- /views/error500.php: -------------------------------------------------------------------------------- 1 | set('error_number', $error_number); 20 | if(isset($active_user) && is_object($active_user) && isset($e)) { 21 | if($active_user->developer) { 22 | $content->set('exception_class', get_class($e)); 23 | $content->set('error_details', $e); 24 | } 25 | } 26 | 27 | $page = new PageSection('base'); 28 | $page->set('title', 'An error occurred'); 29 | $page->set('content', $content); 30 | $page->set('alerts', array()); 31 | header('HTTP/1.1 500 Internal Server Error'); 32 | echo $page->generate(); 33 | -------------------------------------------------------------------------------- /views/error503.php: -------------------------------------------------------------------------------- 1 | set('title', 'Down for maintenance'); 22 | $page->set('content', $content); 23 | $page->set('alerts', array()); 24 | header('HTTP/1.1 503 Service Unavailable'); 25 | echo $page->generate(); 26 | -------------------------------------------------------------------------------- /views/error503_upstream.php: -------------------------------------------------------------------------------- 1 | set('title', 'DNS server communication failure'); 22 | $page->set('content', $content); 23 | $page->set('alerts', array()); 24 | header('HTTP/1.1 503 Service Unavailable'); 25 | echo $page->generate(); 26 | -------------------------------------------------------------------------------- /views/home.php: -------------------------------------------------------------------------------- 1 | admin) { 19 | require('views/error403.php'); 20 | exit; 21 | } 22 | 23 | $replication_types = $replication_type_dir->list_replication_types(); 24 | $ns_templates = $template_dir->list_ns_templates(); 25 | $soa_templates = $template_dir->list_soa_templates(); 26 | 27 | if($_SERVER['REQUEST_METHOD'] == 'POST') { 28 | if(isset($_POST['update_settings'])) { 29 | if($_POST['default_replication_type'] === '') { 30 | $type = null; 31 | } else { 32 | $type = $replication_type_dir->get_replication_type_by_id($_POST['default_replication_type']); 33 | } 34 | $replication_type_dir->set_default_replication_type($type); 35 | 36 | if($_POST['default_soa_template'] === '') { 37 | $template = null; 38 | } else { 39 | $template = $template_dir->get_soa_template_by_id($_POST['default_soa_template']); 40 | } 41 | $template_dir->set_default_soa_template($template); 42 | 43 | if($_POST['default_ns_template'] === '') { 44 | $template = null; 45 | } else { 46 | $template = $template_dir->get_ns_template_by_id($_POST['default_ns_template']); 47 | } 48 | $template_dir->set_default_ns_template($template); 49 | 50 | $alert = new UserAlert; 51 | $alert->content = "Settings updated."; 52 | $active_user->add_alert($alert); 53 | 54 | redirect(); 55 | } 56 | } 57 | 58 | $content = new PageSection('settings'); 59 | $content->set('replication_types', $replication_types); 60 | $content->set('ns_templates', $ns_templates); 61 | $content->set('soa_templates', $soa_templates); 62 | 63 | $page = new PageSection('base'); 64 | $page->set('title', 'Settings'); 65 | $page->set('content', $content); 66 | $page->set('alerts', $active_user->pop_alerts()); 67 | 68 | echo $page->generate(); 69 | -------------------------------------------------------------------------------- /views/template.php: -------------------------------------------------------------------------------- 1 | admin) { 19 | require('views/error403.php'); 20 | die; 21 | } 22 | 23 | $type = $router->vars['type']; 24 | $name = $router->vars['name']; 25 | 26 | try { 27 | if($type == 'soa') { 28 | $template = $template_dir->get_soa_template_by_name($name); 29 | } elseif($type == 'ns') { 30 | $template = $template_dir->get_ns_template_by_name($name); 31 | } else { 32 | require('views/error404.php'); 33 | die; 34 | } 35 | } catch(TemplateNotFound $e) { 36 | require('views/error404.php'); 37 | die; 38 | } 39 | if($_SERVER['REQUEST_METHOD'] == 'POST') { 40 | if(isset($_POST['update_template'])) { 41 | $template->name = trim($_POST['name']); 42 | if($type == 'soa') { 43 | $template->primary_ns = $_POST['primary_ns']; 44 | $template->contact = $_POST['contact']; 45 | $template->refresh = DNSTime::expand($_POST['refresh']); 46 | $template->retry = DNSTime::expand($_POST['retry']); 47 | $template->expire = DNSTime::expand($_POST['expire']); 48 | $template->default_ttl = DNSTime::expand($_POST['default_ttl']); 49 | $template->soa_ttl = DNSTime::expand($_POST['soa_ttl']); 50 | } elseif($type == 'ns') { 51 | $template->nameservers = implode("\n", preg_split('/[,\s]+/', trim($_POST['nameservers']))); 52 | } 53 | $template->update(); 54 | $alert = new UserAlert; 55 | $alert->content = "Template updated."; 56 | $active_user->add_alert($alert); 57 | } 58 | redirect('/templates/'.urlencode($type).'/'.urlencode($template->name)); 59 | } 60 | 61 | $content = new PageSection('template'); 62 | $content->set('type', $type); 63 | $content->set('template', $template); 64 | 65 | $page = new PageSection('base'); 66 | $page->set('title', strtoupper($type).' template: '.$name); 67 | $page->set('content', $content); 68 | $page->set('alerts', $active_user->pop_alerts()); 69 | 70 | echo $page->generate(); 71 | -------------------------------------------------------------------------------- /views/templates.php: -------------------------------------------------------------------------------- 1 | admin) { 19 | require('views/error403.php'); 20 | exit; 21 | } 22 | 23 | $type = null; 24 | $title = 'Templates'; 25 | if(isset($router->vars['type'])) { 26 | $type = $router->vars['type']; 27 | $title = strtoupper($type).' templates'; 28 | } 29 | 30 | if($_SERVER['REQUEST_METHOD'] == 'POST') { 31 | if(isset($_POST['create_template'])) { 32 | if($type == 'soa') { 33 | $template = new SOATemplate; 34 | $template->name = trim($_POST['name']); 35 | $template->primary_ns = $_POST['primary_ns']; 36 | $template->contact = $_POST['contact']; 37 | $template->refresh = DNSTime::expand($_POST['refresh']); 38 | $template->retry = DNSTime::expand($_POST['retry']); 39 | $template->expire = DNSTime::expand($_POST['expire']); 40 | $template->default_ttl = DNSTime::expand($_POST['default_ttl']); 41 | $template->soa_ttl = DNSTime::expand($_POST['soa_ttl']); 42 | } elseif($type == 'ns') { 43 | $template = new NSTemplate; 44 | $template->name = trim($_POST['name']); 45 | $template->nameservers = implode("\n", preg_split('/[,\s]+/', trim($_POST['nameservers']))); 46 | } 47 | $template_dir->add_template($template); 48 | $alert = new UserAlert; 49 | $alert->content = "Template created."; 50 | $active_user->add_alert($alert); 51 | } elseif(isset($_POST['set_default_soa_template'])) { 52 | $template = $template_dir->get_soa_template_by_id($_POST['set_default_soa_template']); 53 | $template_dir->set_default_soa_template($template); 54 | $alert = new UserAlert; 55 | $alert->content = "New SOA default set."; 56 | $active_user->add_alert($alert); 57 | } elseif(isset($_POST['set_default_ns_template'])) { 58 | $template = $template_dir->get_ns_template_by_id($_POST['set_default_ns_template']); 59 | $template_dir->set_default_ns_template($template); 60 | $alert = new UserAlert; 61 | $alert->content = "New NS default set."; 62 | $active_user->add_alert($alert); 63 | } elseif(isset($_POST['delete_soa_template'])) { 64 | $template = $template_dir->get_soa_template_by_id($_POST['delete_soa_template']); 65 | $template_dir->delete_template($template); 66 | $alert = new UserAlert; 67 | $alert->content = "SOA template deleted."; 68 | $active_user->add_alert($alert); 69 | } elseif(isset($_POST['delete_ns_template'])) { 70 | $template = $template_dir->get_ns_template_by_id($_POST['delete_ns_template']); 71 | $template_dir->delete_template($template); 72 | $alert = new UserAlert; 73 | $alert->content = "NS template deleted."; 74 | $active_user->add_alert($alert); 75 | } 76 | redirect('/templates/'.urlencode($type).'#list'); 77 | } 78 | $soa_templates = $template_dir->list_soa_templates(); 79 | $ns_templates = $template_dir->list_ns_templates(); 80 | 81 | $content = new PageSection('templates'); 82 | $content->set('title', $title); 83 | $content->set('soa_templates', $soa_templates); 84 | $content->set('ns_templates', $ns_templates); 85 | $content->set('type', $type); 86 | 87 | $page = new PageSection('base'); 88 | $page->set('title', $title); 89 | $page->set('content', $content); 90 | $page->set('alerts', $active_user->pop_alerts()); 91 | 92 | echo $page->generate(); 93 | -------------------------------------------------------------------------------- /views/user.php: -------------------------------------------------------------------------------- 1 | get_user_by_uid($router->vars['uid']); 20 | } catch(UserNotFound $e) { 21 | require('views/error404.php'); 22 | die; 23 | } 24 | 25 | if($_SERVER['REQUEST_METHOD'] == 'POST') { 26 | if(isset($_POST['update_user']) && $active_user->admin) { 27 | $user->name = $_POST['name']; 28 | $user->email = $_POST['email']; 29 | $user->active = isset($_POST['active']) ? 1 : 0; 30 | $user->admin = isset($_POST['admin']) ? 1 : 0; 31 | $user->update(); 32 | $alert = new UserAlert; 33 | $alert->content = "User '{$user->uid}' updated."; 34 | $alert->class = 'success'; 35 | $active_user->add_alert($alert); 36 | redirect(); 37 | } 38 | } 39 | $changesets = $user->list_changesets(); 40 | $zones = $active_user->list_accessible_zones(); 41 | $visible_changesets = array(); 42 | foreach($changesets as $changeset) { 43 | if(isset($zones[$changeset->zone->pdns_id])) { 44 | $visible_changesets[] = $changeset; 45 | } 46 | } 47 | if(count($visible_changesets) == 0 && !$active_user->admin) { 48 | require('views/error404.php'); 49 | die; 50 | } 51 | 52 | $content = new PageSection('user'); 53 | $content->set('user', $user); 54 | $content->set('changesets', $visible_changesets); 55 | 56 | $page = new PageSection('base'); 57 | $page->set('title', $user->name); 58 | $page->set('content', $content); 59 | $page->set('alerts', $active_user->pop_alerts()); 60 | 61 | echo $page->generate(); 62 | -------------------------------------------------------------------------------- /views/users.php: -------------------------------------------------------------------------------- 1 | list_users(); 19 | 20 | if(!$active_user->admin) { 21 | require('views/error403.php'); 22 | die; 23 | } 24 | 25 | if($_SERVER['REQUEST_METHOD'] == 'POST') { 26 | if(isset($_POST['add_user']) && $active_user->admin) { 27 | $user = new User; 28 | $user->auth_realm = 'local'; 29 | $user->uid = $_POST['uid']; 30 | $user->name = $_POST['name']; 31 | $user->email = $_POST['email']; 32 | $user->active = 1; 33 | $user->admin = isset($_POST['admin']) ? 1 : 0; 34 | try { 35 | $user_dir->add_user($user); 36 | $alert = new UserAlert; 37 | $alert->content = 'User \''.hesc($user->uid).'\' added.'; 38 | $alert->escaping = ESC_NONE; 39 | $alert->class = 'success'; 40 | $active_user->add_alert($alert); 41 | } catch(UserAlreadyExistsException $e) { 42 | $alert = new UserAlert; 43 | $alert->content = 'A user with user ID of \''.hesc($user->uid).'\' already exists.'; 44 | $alert->escaping = ESC_NONE; 45 | $alert->class = 'danger'; 46 | $active_user->add_alert($alert); 47 | } 48 | redirect(); 49 | } 50 | } 51 | 52 | $content = new PageSection('users'); 53 | $content->set('users', $users); 54 | 55 | $page = new PageSection('base'); 56 | $page->set('title', 'Users'); 57 | $page->set('content', $content); 58 | $page->set('alerts', $active_user->pop_alerts()); 59 | 60 | echo $page->generate(); 61 | -------------------------------------------------------------------------------- /views/zoneexport.php: -------------------------------------------------------------------------------- 1 | get_zone_by_name($router->vars['name'].'.'); 20 | } catch(ZoneNotFound $e) { 21 | require('views/error404.php'); 22 | exit; 23 | } 24 | 25 | if(!$active_user->admin && !$active_user->access_to($zone)) { 26 | require('views/error403.php'); 27 | exit; 28 | } 29 | 30 | $rrsets = $zone->list_resource_record_sets(); 31 | 32 | $page = new PageSection('zoneexport'); 33 | $page->set('zone', $zone); 34 | $page->set('rrsets', $rrsets); 35 | header('Content-type: text/plain; charset=utf-8'); 36 | header('Content-disposition: attachment; filename='.DNSZoneName::unqualify($zone->name)); 37 | echo $page->generate(); 38 | -------------------------------------------------------------------------------- /views/zoneimport.php: -------------------------------------------------------------------------------- 1 | get_zone_by_name($router->vars['name'].'.'); 20 | } catch(ZoneNotFound $e) { 21 | require('views/error404.php'); 22 | exit; 23 | } 24 | 25 | if(!$active_user->admin && !$active_user->access_to($zone)) { 26 | require('views/error403.php'); 27 | exit; 28 | } 29 | 30 | if(isset($_FILES['zonefile'])) { 31 | $lines = file_get_contents($_FILES['zonefile']['tmp_name']); 32 | $zonefile = new BindZonefile($lines); 33 | try { 34 | $new_rrsets = $zonefile->parse_into_rrsets($zone, $_POST['comment_handling']); 35 | $modifications = merge_rrsets($zone, $new_rrsets); 36 | } catch(ZoneImportError $e) { 37 | $content = new PageSection('zone_update_failed'); 38 | $content->set('message', $e->getMessage()); 39 | } 40 | if(!isset($content)) { 41 | $content = new PageSection('zoneimport'); 42 | $content->set('zone', $zone); 43 | $content->set('modifications', $modifications); 44 | } 45 | } else { 46 | redirect('/zones/'.urlencode($zone->name)); 47 | } 48 | 49 | $page = new PageSection('base'); 50 | $page->set('title', 'Import preview for '.DNSZoneName::unqualify(punycode_to_utf8($zone->name)).' zone update'); 51 | $page->set('content', $content); 52 | $page->set('alerts', $active_user->pop_alerts()); 53 | 54 | echo $page->generate(); 55 | 56 | function merge_rrsets($zone, $new_rrsets) { 57 | global $active_user; 58 | // Compare existing content with new content and collate the differences 59 | $rrsets = $zone->list_resource_record_sets(); 60 | $old_rrsets = $rrsets; 61 | $modifications = array('add' => array(), 'update' => array(), 'delete' => array()); 62 | foreach($new_rrsets as $ref => $new_rrset) { 63 | if($new_rrset->type == 'SOA') continue; 64 | if(isset($old_rrsets[$ref])) { 65 | $old_rrset = $old_rrsets[$ref]; 66 | $old_rrs = $old_rrset->list_resource_records(); 67 | $new_rrs = $new_rrset->list_resource_records(); 68 | $old_comment = $old_rrset->merge_comment_text(); 69 | $new_comment = $new_rrset->merge_comment_text(); 70 | $rrset_modifications = array(); 71 | if($old_rrset->ttl != $new_rrset->ttl) { 72 | $rrset_modifications[] = 'TTL changed from '.DNSTime::abbreviate($old_rrset->ttl).' to '.DNSTime::abbreviate($new_rrset->ttl); 73 | } 74 | foreach($new_rrs as $new_rr) { 75 | $rr_match = false; 76 | foreach($old_rrs as $rr_ref => $old_rr) { 77 | if($new_rr->content == $old_rr->content) { 78 | $rr_match = true; 79 | unset($old_rrs[$rr_ref]); 80 | break; 81 | } 82 | } 83 | if($rr_match) { 84 | if($new_rr->disabled && !$old_rr->disabled) { 85 | $rrset_modifications[] = 'Disabled RR: '.$new_rr->content; 86 | } 87 | if(!$new_rr->disabled && $old_rr->disabled) { 88 | $rrset_modifications[] = 'Enabled RR: '.$new_rr->content; 89 | } 90 | } else { 91 | // New RR 92 | $rrset_modifications[] = 'New RR: '.$new_rr->content; 93 | } 94 | } 95 | foreach($old_rrs as $old_rr) { 96 | // Deleted RR 97 | $rrset_modifications[] = 'Deleted RR: '.$old_rr->content; 98 | } 99 | $new_rrset->clear_comments(); 100 | if($old_comment == $new_comment) { 101 | foreach($old_rrset->list_comments() as $comment) { 102 | $new_rrset->add_comment($comment); 103 | } 104 | } else { 105 | if($old_comment != '') $rrset_modifications[] = 'Deleted comment: '.$old_comment; 106 | if($new_comment != '') { 107 | $rrset_modifications[] = 'New comment: '.$new_comment; 108 | $comment = new Comment; 109 | $comment->content = $new_comment; 110 | $comment->account = $active_user->uid; 111 | $new_rrset->add_comment($comment); 112 | } 113 | } 114 | if(count($rrset_modifications) > 0) { 115 | $modifications['update'][$ref] = array(); 116 | $modifications['update'][$ref]['new'] = $new_rrset; 117 | $modifications['update'][$ref]['changelist'] = $rrset_modifications; 118 | $modifications['update'][$ref]['json'] = build_json('update', $new_rrset, $zone->name); 119 | } 120 | } else { 121 | // New RRSet 122 | $modifications['add'][$ref] = array(); 123 | $modifications['add'][$ref]['new'] = $new_rrset; 124 | $modifications['add'][$ref]['json'] = build_json('add', $new_rrset, $zone->name); 125 | } 126 | } 127 | foreach($old_rrsets as $ref => $old_rrset) { 128 | if($old_rrset->type == 'SOA') continue; 129 | if(!isset($new_rrsets[$ref])) { 130 | // Deleted RRSet 131 | $modifications['delete'][$ref] = array(); 132 | $modifications['delete'][$ref]['old'] = $old_rrset; 133 | $modifications['delete'][$ref]['json'] = build_json('delete', $old_rrset, $zone->name); 134 | } 135 | } 136 | return $modifications; 137 | } 138 | 139 | function build_json($action, $rrset, $zonename) { 140 | $data = new StdClass; 141 | $data->action = $action; 142 | $data->name = DNSName::abbreviate($rrset->name, $zonename); 143 | $data->type = $rrset->type; 144 | $data->ttl = $rrset->ttl; 145 | if($action != 'add') { 146 | $data->oldname = $data->name; 147 | $data->oldtype = $data->type; 148 | } 149 | if($action != 'delete') { 150 | $data->records = array(); 151 | foreach($rrset->list_resource_records() as $rr) { 152 | $rr_data = new StdClass; 153 | $rr_data->content = DNSContent::decode($rr->content, $rrset->type, $zonename); 154 | $rr_data->enabled = $rr->disabled ? 'No' : 'Yes'; 155 | $data->records[] = $rr_data; 156 | } 157 | $data->comment = $rrset->merge_comment_text(); 158 | } 159 | return json_encode($data); 160 | } 161 | 162 | -------------------------------------------------------------------------------- /views/zones.php: -------------------------------------------------------------------------------- 1 | list_accessible_zones(array('pending_updates')); 19 | usort($zones, function($a, $b) { 20 | $aname = implode(',', array_reverse(explode('.', punycode_to_utf8($a->name)))); 21 | $bname = implode(',', array_reverse(explode('.', punycode_to_utf8($b->name)))); 22 | return strnatcasecmp($aname, $bname); 23 | }); 24 | 25 | $replication_types = $replication_type_dir->list_replication_types(); 26 | $soa_templates = $template_dir->list_soa_templates(); 27 | $ns_templates = $template_dir->list_ns_templates(); 28 | $account_whitelist = !empty($config['dns']['classification_whitelist']) ? explode(',', $config['dns']['classification_whitelist']) : []; 29 | $force_account_whitelist = !empty($config['dns']['classification_whitelist']) ? 1 : 0; 30 | 31 | if($_SERVER['REQUEST_METHOD'] == 'POST') { 32 | if(isset($_POST['add_zone']) && $active_user->admin) { 33 | $zonename = utf8_to_punycode(rtrim(trim($_POST['name']), '.')).'.'; 34 | if(strlen($zonename) == 1) { 35 | $content = new PageSection('zone_add_failed'); 36 | $content->set('message', 'No zone name given.'); 37 | } else { 38 | $zone = new Zone; 39 | $zone->name = $zonename; 40 | $zone->account = trim($_POST['classification']); 41 | $zone->dnssec = isset($_POST['dnssec']) ? 1 : 0; 42 | $zone->kind = $_POST['kind']; 43 | $zone->nameservers = array(); 44 | foreach(preg_split('/[,\s]+/', $_POST['nameservers']) as $nameserver) { 45 | $zone->nameservers[] = $nameserver; 46 | } 47 | $soa = new ResourceRecord; 48 | $soa->content = "$_POST[primary_ns] $_POST[contact] ".date('Ymd00')." ".DNSTime::expand($_POST['refresh'])." ".DNSTime::expand($_POST['retry'])." ".DNSTime::expand($_POST['expire'])." ".DNSTime::expand($_POST['default_ttl']); 49 | $soa->disabled = false; 50 | $soaset = new ResourceRecordSet; 51 | $soaset->name = $zonename; 52 | $soaset->type = 'SOA'; 53 | $soaset->ttl = DNSTime::expand($_POST['soa_ttl']); 54 | $soaset->add_resource_record($soa); 55 | $zone->add_resource_record_set($soaset); 56 | try { 57 | $zone_dir->create_zone($zone); 58 | redirect('/zones/'.urlencode(DNSZoneName::unqualify($zonename))); 59 | } catch(Pest_InvalidRecord $e) { 60 | $content = new PageSection('zone_add_failed'); 61 | $content->set('message', json_decode($e->getMessage())->error); 62 | } 63 | } 64 | } 65 | } 66 | 67 | if(!isset($content)) { 68 | $content = new PageSection('zones'); 69 | $content->set('zones', $zones); 70 | $content->set('replication_types', $replication_types); 71 | $content->set('soa_templates', $soa_templates); 72 | $content->set('ns_templates', $ns_templates); 73 | $content->set('dnssec_enabled', isset($config['dns']['dnssec']) ? $config['dns']['dnssec'] : '0'); 74 | $content->set('dnssec_edit', isset($config['dns']['dnssec_edit']) ? $config['dns']['dnssec_edit'] : '0'); 75 | $content->set('account_whitelist', $account_whitelist); 76 | $content->set('force_account_whitelist', $force_account_whitelist); 77 | } 78 | 79 | $page = new PageSection('base'); 80 | $page->set('title', 'Zones'); 81 | $page->set('content', $content); 82 | $page->set('alerts', $active_user->pop_alerts()); 83 | 84 | echo $page->generate(); 85 | -------------------------------------------------------------------------------- /views/zonesplit.php: -------------------------------------------------------------------------------- 1 | get_zone_by_name($router->vars['name'].'.'); 20 | } catch(ZoneNotFound $e) { 21 | require('views/error404.php'); 22 | exit; 23 | } 24 | 25 | if(!$active_user->admin) { 26 | require('views/error403.php'); 27 | exit; 28 | } 29 | 30 | $rrsets = $zone->list_resource_record_sets(); 31 | 32 | if($_SERVER['REQUEST_METHOD'] == 'POST') { 33 | if(isset($_POST['suffix'])) { 34 | $newzonename = utf8_to_punycode($_POST['suffix']).'.'.$zone->name; 35 | $split = array(); 36 | $cname_error = false; 37 | foreach($rrsets as $rrset) { 38 | if((stripos($rrset->name, '.'.$newzonename) === strlen($rrset->name) - strlen('.'.$newzonename) 39 | || ($rrset->name == $newzonename && $rrset->type != 'NS')) && $rrset->type != 'SOA') { 40 | if($rrset->name == $newzonename && $rrset->type == 'CNAME') { 41 | $cname_error = true; 42 | $alert = new UserAlert; 43 | $alert->content = "It is not possible to have a CNAME record at the root of the zone. You will need to change the highlighted CNAME record into an A/AAAA record before proceeding."; 44 | $alert->class = "danger"; 45 | $active_user->add_alert($alert); 46 | } 47 | $split[] = $rrset; 48 | } 49 | } 50 | if(isset($_POST['confirm']) && count($split) > 0 && !$cname_error) { 51 | // Build new zone with split records 52 | // Copy nameservers and SOA from old zone 53 | $newzone = new Zone; 54 | $newzone->name = $newzonename; 55 | $newzone->account = $zone->account; 56 | $newzone->dnssec = $zone->dnssec; 57 | $newzone->kind = 'Master'; 58 | $newzone->nameservers = $zone->nameservers; 59 | foreach($split as $rrset) { 60 | $newzone->add_resource_record_set($rrset); 61 | } 62 | $soa = new ResourceRecord; 63 | $soa->content = $zone->soa->content; 64 | $soa->disabled = false; 65 | $soaset = new ResourceRecordSet; 66 | $soaset->name = $newzonename; 67 | $soaset->type = 'SOA'; 68 | $soaset->ttl = $zone->soa->ttl; 69 | $soaset->add_resource_record($soa); 70 | $newzone->add_resource_record_set($soaset); 71 | try { 72 | // Create new zone 73 | $zone_dir->create_zone($newzone); 74 | // Update old zone (remove split records) 75 | $changes = array(); 76 | foreach($split as $rrset) { 77 | $change = new Change; 78 | $change->before = serialize($rrset); 79 | $changes[] = $change; 80 | $zone->delete_resource_record_set($rrset); 81 | } 82 | $zone->commit_changes(); 83 | $changeset = new ChangeSet; 84 | if(!empty($_POST['comment'])) { 85 | $changeset->comment = $_POST['comment']; 86 | } 87 | $zone->add_changeset($changeset); 88 | foreach($changes as $change) { 89 | $changeset->add_change($change); 90 | } 91 | // Create git commit for this change 92 | $git_commit_comment = "Zone {$newzonename} split off from {$zone->name} via DNS UI\n"; 93 | $git_commit_comment .= "\n{$config['web']['baseurl']}/zones/".urlencode($zone->name).'#changelog#'.$changeset->id; 94 | if(!empty($_POST['comment'])) { 95 | $git_commit_comment .= "\nChange comment: {$_POST['comment']}"; 96 | } 97 | $zone_dir->git_tracked_export(array($zone), $git_commit_comment); 98 | $alert = new UserAlert; 99 | $alert->content = "Zone split of ".DNSZoneName::unqualify($newzonename)." from ".DNSZoneName::unqualify($zone->name)." has been completed."; 100 | $active_user->add_alert($alert); 101 | $content = new PageSection('zonesplitcompleted'); 102 | $content->set('zone', $zone); 103 | $content->set('newzonename', $newzonename); 104 | } catch(Pest_InvalidRecord $e) { 105 | $content = new PageSection('zone_add_failed'); 106 | $content->set('message', json_decode($e->getMessage())->error); 107 | } 108 | } else { 109 | $content = new PageSection('zonesplit'); 110 | $content->set('zone', $zone); 111 | $content->set('newzonename', $newzonename); 112 | $content->set('suffix', $_POST['suffix']); 113 | $content->set('split', $split); 114 | $content->set('cname_error', $cname_error); 115 | } 116 | } 117 | } 118 | 119 | $page = new PageSection('base'); 120 | $page->set('title', 'Split preview for '.$zone->name); 121 | $page->set('content', $content); 122 | $page->set('alerts', $active_user->pop_alerts()); 123 | 124 | echo $page->generate(); 125 | 126 | --------------------------------------------------------------------------------