├── .editorconfig ├── .gitignore ├── .htaccess ├── LICENSE ├── README.md ├── ddns_token_del.php ├── ddns_token_edit.php ├── ddns_token_list.php ├── form └── ddns_token.tform.php ├── lib ├── admin.conf.php ├── classes │ └── ddns_custom_datasource.inc.php ├── ddns.menu.php ├── lang │ ├── de_ddns_token.lng │ ├── de_ddns_token_list.lng │ ├── en_ddns_token.lng │ └── en_ddns_token_list.lng └── updater │ ├── DdnsUpdater.php │ ├── request │ ├── DdnsRequest.php │ ├── DefaultDdnsRequest.php │ └── DynDnsRequest.php │ ├── response │ ├── DdnsResponseWriter.php │ ├── DefaultDdnsResponseWriter.php │ ├── DynDns1ResponseWriter.php │ └── DynDns2ResponseWriter.php │ └── token │ └── DdnsToken.php ├── list └── ddns_token.list.php ├── migration_1.4.0.sql ├── nic └── .htaccess ├── setup.sql ├── templates ├── ddns_token_edit.htm └── ddns_token_list.htm ├── update.config.local-example.php ├── update.config.php └── update.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 4 12 | max_line_length = 120 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # idea 2 | *.iml 3 | .idea/ 4 | update.config.local.php 5 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | # workaround for PHP in CGI mode to pass Authorization header (ddns token) to the PHP scripts 2 | RewriteEngine On 3 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Marcel Hofer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ISPConfig 3 Dynamic DNS (DDNS) Module 2 | 3 | - [Features](#features) 4 | - [Screenshots](#screenshots) 5 | - [Installation](#installation) 6 | - [Update](#update) 7 | - [Uninstall](#uninstall) 8 | - [Known/Unknown Issues](#knownunknown-issues) 9 | 10 | For general questions or feedback use the [forum thread on howtoforge](https://www.howtoforge.com/community/threads/ispconfig-3-danymic-dns-ddns-module.87967/). 11 | 12 | ## Features 13 | - Integrated into your ISPConfig 3 DNS menu 14 | - Allows clients, resellers and admins to create ddns tokens 15 | - Updates can be performed with simple GET requests using these tokens 16 | - Updates can be performed using DynDns1 and DynDns2 protocols 17 | - Tokens can be restricted to individual DNS zones, DNS records and records types (A/AAAA) 18 | - Allows updating A (IPv4) and AAAA (IPv6) records 19 | - Allows creating/deleting TXT records. Useful for ACME dns-01 challenges using the custom [certbot-dns-ispconfig-ddns plugin](https://github.com/mhofer117/certbot-dns-ispconfig-ddns) 20 | - The update script shares the same authentication rate-limiting / blocking method from base ISPConfig 21 | - Works in multi-server setups if the DB-Table is created on all servers, [see discussion](https://github.com/mhofer117/ispconfig-ddns-module/issues/4#issuecomment-1437604492) 22 | 23 | ## Screenshots 24 |  25 |  26 |  27 | 28 | 29 | 30 | ## Installation 31 | - Create the database table using [`setup.sql`](setup.sql) inside of your existing ispconfig database, usually called "dbispconfig" 32 | - Checkout the repository or download and extract a release on your server 33 | - Move the directory to the correct location: `mv ispconfig-ddns-module /usr/local/ispconfig/interface/web/ddns` 34 | - Set permissions and create symlinks as follows: 35 | ```` 36 | # install module 37 | chown -R ispconfig:ispconfig /usr/local/ispconfig/interface/web/ddns 38 | # setup dependency class 39 | ln -s -f /usr/local/ispconfig/interface/web/ddns/lib/classes/ddns_custom_datasource.inc.php /usr/local/ispconfig/interface/lib/classes/ 40 | chown -h ispconfig:ispconfig /usr/local/ispconfig/interface/lib/classes/ddns_custom_datasource.inc.php 41 | # link menu entries in DNS module 42 | mkdir -p /usr/local/ispconfig/interface/web/dns/lib/menu.d 43 | ln -s -f /usr/local/ispconfig/interface/web/ddns/lib/ddns.menu.php /usr/local/ispconfig/interface/web/dns/lib/menu.d/ 44 | chown -h ispconfig:ispconfig /usr/local/ispconfig/interface/web/dns/lib/menu.d/ddns.menu.php 45 | # link nic directory to support dyndns v1/v2 protocol endpoints 46 | ln -s -f /usr/local/ispconfig/interface/web/ddns/nic /usr/local/ispconfig/interface/web/ 47 | chown -h ispconfig:ispconfig /usr/local/ispconfig/interface/web/nic 48 | ```` 49 | 50 | If you are using nginx, [you will also need to set up a proxy host](https://github.com/mhofer117/ispconfig-ddns-module/wiki/Setup-Proxy-Domain-(nginx)) 51 | 52 | ## Update 53 | If you pulled the module to your server with git, use `git pull`, otherwise download the latest release and override all existing files. 54 | After that, re-run the commands from the installation steps to fix permissions / symlinks. 55 | 56 | ## Uninstall 57 | - Remove module database table ``DROP TABLE IF EXISTS `ddns_token`;`` 58 | - Delete all module files and related symlinks 59 | ```` 60 | rm -f /usr/local/ispconfig/interface/lib/classes/ddns_custom_datasource.inc.php 61 | rm -f /usr/local/ispconfig/interface/web/dns/lib/menu.d/ddns.menu.php 62 | rmdir /usr/local/ispconfig/interface/web/dns/lib/menu.d 63 | rm -rf /usr/local/ispconfig/interface/web/ddns 64 | rm -rf /usr/local/ispconfig/interface/web/nic 65 | ```` 66 | 67 | ## Known Issues 68 | - Paging does not work correctly, show all records on the same page to work around this 69 | - nginx will not respect the .htaccess file used for DynDns, you need to set up a revers proxy domain, 70 | [see the guide in the wiki](https://github.com/mhofer117/ispconfig-ddns-module/wiki/Setup-Proxy-Domain-(nginx)) 71 | - The following clients require ISPConfig on a default port (443 or 80): 72 | - DynDns1 and DynDns2 protocols, for example with [ddclient](https://github.com/ddclient/ddclient) 73 | - FRITZ!Box (tm) (may support :8080 and other ports in a future update) 74 | - maybe others 75 | - Workaround: if you don't want to change the ISPConfig port for some reason, you can set up a proxy domain 76 | [as described in the wiki](https://github.com/mhofer117/ispconfig-ddns-module/wiki/Setup-Proxy-Domain) 77 | -------------------------------------------------------------------------------- /ddns_token_del.php: -------------------------------------------------------------------------------- 1 | auth; 8 | 9 | // From and List definition files 10 | $list_def_file = 'list/ddns_token.list.php'; 11 | $tform_def_file = 'form/ddns_token.tform.php'; 12 | 13 | //* Check permissions for module 14 | $auth->check_module_permissions('dns'); 15 | 16 | // Load the form 17 | $app->uses('tform_actions'); 18 | $app->tform_actions->onDelete(); 19 | 20 | ?> 21 | -------------------------------------------------------------------------------- /ddns_token_edit.php: -------------------------------------------------------------------------------- 1 | auth; 8 | 9 | /****************************************** 10 | * Begin Form configuration 11 | ******************************************/ 12 | 13 | $tform_def_file = 'form/ddns_token.tform.php'; 14 | 15 | /****************************************** 16 | * End Form configuration 17 | ******************************************/ 18 | 19 | //* Check permissions for module 20 | $auth->check_module_permissions('dns'); 21 | 22 | $app->uses('tpl,tform,tform_actions'); 23 | $app->load('tform_actions'); 24 | 25 | // Create a class page_action that extends the tform_actions base class 26 | class page_action extends tform_actions { 27 | 28 | function onBeforeInsert() 29 | { 30 | global $app, $conf; 31 | 32 | if($this->id <= 0) { 33 | try { 34 | // generate 48 character hex string (192 bits of entropy) 35 | $this->dataRecord['token'] = bin2hex(random_bytes(24)); 36 | } catch (Exception $e) { 37 | $app->tform->errorMessage = "Unable to generate random token: " . $e->getMessage(); 38 | } 39 | } 40 | 41 | parent::onBeforeInsert(); 42 | } 43 | 44 | function onShowEnd() { 45 | global $app, $conf; 46 | // If user is admin, we will allow him to select to whom this record belongs 47 | if($_SESSION["s"]["user"]["typ"] == 'admin') { 48 | // Getting all users 49 | $sql = "SELECT sys_group.groupid, sys_group.name, CONCAT(IF(client.company_name != '', CONCAT(client.company_name, ' :: '), ''), client.contact_name, ' (', client.username, IF(client.customer_no != '', CONCAT(', ', client.customer_no), ''), ')') as contactname FROM sys_group, client WHERE sys_group.client_id = client.client_id AND sys_group.client_id > 0 ORDER BY client.company_name, client.contact_name, sys_group.name"; 50 | $clients = $app->db->queryAllRecords($sql); 51 | $clients = $app->functions->htmlentities($clients); 52 | $client_select = ''; 53 | if($_SESSION["s"]["user"]["typ"] == 'admin') $client_select .= ""; 54 | if(is_array($clients)) { 55 | foreach( $clients as $client) { 56 | $selected = @(is_array($this->dataRecord) && ($client["groupid"] == $this->dataRecord['client_group_id'] || $client["groupid"] == $this->dataRecord['sys_groupid']))?'SELECTED':''; 57 | $client_select .= "\r\n"; 58 | } 59 | } 60 | $app->tpl->setVar("client_group_id", $client_select); 61 | } else if($app->auth->has_clients($_SESSION['s']['user']['userid'])) { 62 | 63 | // Get the limits of the client 64 | $client_group_id = intval($_SESSION["s"]["user"]["default_group"]); 65 | $client = $app->db->queryOneRecord("SELECT client.client_id, client.contact_name, CONCAT(IF(client.company_name != '', CONCAT(client.company_name, ' :: '), ''), client.contact_name, ' (', client.username, IF(client.customer_no != '', CONCAT(', ', client.customer_no), ''), ')') as contactname, sys_group.name FROM sys_group, client WHERE sys_group.client_id = client.client_id and sys_group.groupid = ?", $client_group_id); 66 | $client = $app->functions->htmlentities($client); 67 | 68 | // Fill the client select field 69 | $sql = "SELECT sys_group.groupid, sys_group.name, CONCAT(IF(client.company_name != '', CONCAT(client.company_name, ' :: '), ''), client.contact_name, ' (', client.username, IF(client.customer_no != '', CONCAT(', ', client.customer_no), ''), ')') as contactname FROM sys_group, client WHERE sys_group.client_id = client.client_id AND client.parent_client_id = ? ORDER BY client.company_name, client.contact_name, sys_group.name"; 70 | $clients = $app->db->queryAllRecords($sql, $client['client_id']); 71 | $clients = $app->functions->htmlentities($clients); 72 | $tmp = $app->db->queryOneRecord("SELECT groupid FROM sys_group WHERE client_id = ?", $client['client_id']); 73 | $client_select = ''; 74 | //$tmp_data_record = $app->tform->getDataRecord($this->id); 75 | if(is_array($clients)) { 76 | foreach( $clients as $client) { 77 | $selected = @(is_array($this->dataRecord) && ($client["groupid"] == $this->dataRecord['client_group_id'] || $client["groupid"] == $this->dataRecord['sys_groupid']))?'SELECTED':''; 78 | $client_select .= "\r\n"; 79 | } 80 | } 81 | $app->tpl->setVar("client_group_id", $client_select); 82 | 83 | } 84 | parent::onShowEnd(); 85 | } 86 | 87 | function onSubmit() { 88 | global $app, $conf; 89 | 90 | # statically limit the record to this server only (issue #11) 91 | $this->dataRecord['server_id'] = $conf['server_id']; 92 | 93 | if($_SESSION['s']['user']['typ'] != 'admin' && !$app->auth->has_clients($_SESSION['s']['user']['userid'])) unset($this->dataRecord["client_group_id"]); 94 | 95 | parent::onSubmit(); 96 | } 97 | 98 | function onAfterInsert() { 99 | global $app, $conf; 100 | 101 | if($_SESSION["s"]["user"]["typ"] == 'admin' && isset($this->dataRecord["client_group_id"])) { 102 | $client_group_id = $app->functions->intval($this->dataRecord["client_group_id"]); 103 | $app->db->query("UPDATE ddns_token SET sys_groupid = ?, sys_perm_group = 'riud' WHERE id = ?", $client_group_id, $this->id); 104 | } 105 | if($app->auth->has_clients($_SESSION['s']['user']['userid']) && isset($this->dataRecord["client_group_id"])) { 106 | $client_group_id = intval($_SESSION["s"]["user"]["default_group"]); 107 | $client = $app->db->queryOneRecord("SELECT client.client_id, client.contact_name, CONCAT(IF(client.company_name != '', CONCAT(client.company_name, ' :: '), ''), client.contact_name, ' (', client.username, IF(client.customer_no != '', CONCAT(', ', client.customer_no), ''), ')') as contactname, sys_group.name FROM sys_group, client WHERE sys_group.client_id = client.client_id and sys_group.groupid = ?", $client_group_id); 108 | $client = $app->functions->htmlentities($client); 109 | $sql = "SELECT sys_group.groupid, sys_group.name, CONCAT(IF(client.company_name != '', CONCAT(client.company_name, ' :: '), ''), client.contact_name, ' (', client.username, IF(client.customer_no != '', CONCAT(', ', client.customer_no), ''), ')') as contactname FROM sys_group, client WHERE sys_group.client_id = client.client_id AND client.parent_client_id = ? ORDER BY client.company_name, client.contact_name, sys_group.name"; 110 | $clients = $app->db->queryAllRecords($sql, $client['client_id']); 111 | $clients = $app->functions->htmlentities($clients); 112 | $valid_group_ids = array(); 113 | if(is_array($clients)) { 114 | foreach( $clients as $client) { 115 | array_push($valid_group_ids, $client['groupid']); 116 | } 117 | } 118 | if (array_search($this->dataRecord["client_group_id"], $valid_group_ids)) { 119 | $set_client_group_id = $app->functions->intval($this->dataRecord["client_group_id"]); 120 | $app->db->query("UPDATE ddns_token SET sys_groupid = ?, sys_perm_group = 'riud' WHERE id = ?", $set_client_group_id, $this->id); 121 | } 122 | } 123 | } 124 | 125 | function onAfterUpdate() { 126 | global $app, $conf; 127 | 128 | if($_SESSION["s"]["user"]["typ"] == 'admin' && isset($this->dataRecord["client_group_id"])) { 129 | $client_group_id = $app->functions->intval($this->dataRecord["client_group_id"]); 130 | $app->db->query("UPDATE ddns_token SET sys_groupid = ?, sys_perm_group = 'riud' WHERE id = ?", $client_group_id, $this->id); 131 | } 132 | if($app->auth->has_clients($_SESSION['s']['user']['userid']) && isset($this->dataRecord["client_group_id"])) { 133 | $client_group_id = intval($_SESSION["s"]["user"]["default_group"]); 134 | $client = $app->db->queryOneRecord("SELECT client.client_id, client.contact_name, CONCAT(IF(client.company_name != '', CONCAT(client.company_name, ' :: '), ''), client.contact_name, ' (', client.username, IF(client.customer_no != '', CONCAT(', ', client.customer_no), ''), ')') as contactname, sys_group.name FROM sys_group, client WHERE sys_group.client_id = client.client_id and sys_group.groupid = ?", $client_group_id); 135 | $client = $app->functions->htmlentities($client); 136 | $sql = "SELECT sys_group.groupid, sys_group.name, CONCAT(IF(client.company_name != '', CONCAT(client.company_name, ' :: '), ''), client.contact_name, ' (', client.username, IF(client.customer_no != '', CONCAT(', ', client.customer_no), ''), ')') as contactname FROM sys_group, client WHERE sys_group.client_id = client.client_id AND client.parent_client_id = ? ORDER BY client.company_name, client.contact_name, sys_group.name"; 137 | $clients = $app->db->queryAllRecords($sql, $client['client_id']); 138 | $clients = $app->functions->htmlentities($clients); 139 | $valid_group_ids = array(); 140 | if(is_array($clients)) { 141 | foreach( $clients as $client) { 142 | array_push($valid_group_ids, $client['groupid']); 143 | } 144 | } 145 | if (array_search($this->dataRecord["client_group_id"], $valid_group_ids)) { 146 | $set_client_group_id = $app->functions->intval($this->dataRecord["client_group_id"]); 147 | $app->db->query("UPDATE ddns_token SET sys_groupid = ?, sys_perm_group = 'riud' WHERE id = ?", $set_client_group_id, $this->id); 148 | } 149 | } 150 | } 151 | 152 | } 153 | 154 | $page = new page_action(); 155 | $page->onLoad(); 156 | 157 | ?> 158 | -------------------------------------------------------------------------------- /ddns_token_list.php: -------------------------------------------------------------------------------- 1 | auth; 8 | 9 | /****************************************** 10 | * Begin Form configuration 11 | ******************************************/ 12 | 13 | $list_def_file = 'list/ddns_token.list.php'; 14 | 15 | /****************************************** 16 | * End Form configuration 17 | ******************************************/ 18 | 19 | //* Check permissions for module 20 | $auth->check_module_permissions('dns'); 21 | 22 | $app->uses('listform_actions'); 23 | 24 | // load custom config for PROXY_HOST variable 25 | $default_config = array('PROXY_HOST' => ''); 26 | if (file_exists(dirname(__FILE__) . '/update.config.local.php')) { 27 | $config_local = include dirname(__FILE__) . '/update.config.local.php'; 28 | $config = array_merge($default_config, $config_local); 29 | } else { 30 | $config = $default_config; 31 | } 32 | $app->tpl->setVar('PROXY_HOST', $config['PROXY_HOST']); 33 | 34 | // $app->listform_actions->SQLExtWhere = "dns_soa.access = 'REJECT'"; 35 | // $app->listform_actions->SQLOrderBy = 'ORDER BY dns_soa.origin'; 36 | 37 | $app->listform_actions->onLoad(); 38 | 39 | 40 | ?> 41 | -------------------------------------------------------------------------------- /form/ddns_token.tform.php: -------------------------------------------------------------------------------- 1 | 0 id must match with id of current user 40 | $form['auth_preset']['userid'] = 0; 41 | 42 | // 0 = default groupid of the user, > 0 id must match with groupid of current 43 | $form['auth_preset']['groupid'] = 0; 44 | 45 | // Permissions with the following codes: r = read, i = insert, u = update, d = delete 46 | $form['auth_preset']['perm_user'] = 'riud'; 47 | $form['auth_preset']['perm_group'] = 'riud'; 48 | $form['auth_preset']['perm_other'] = ''; 49 | 50 | // The form definition of the first tab. The name of the tab is called 'message'. We refer 51 | // to this name in the $form['tab_default'] setting above. 52 | $form['tabs']['token'] = array( 53 | 'title' => 'token_txt', // Title of the Tab 54 | 'width' => 150, // Tab width 55 | 'template' => 'templates/ddns_token_edit.htm', // Template file name 56 | 'fields' => array( 57 | //*** BEGIN Datatable columns ********************************** 58 | 'server_id' => array ( 59 | 'datatype' => 'INTEGER', 60 | 'formtype' => 'SELECT', 61 | 'default' => '', 62 | 'value' => '', 63 | 'width' => '30', 64 | 'maxlength' => '255' 65 | ), 66 | 'token' => array( 67 | 'datatype' => 'VARCHAR', 68 | 'formtype' => 'TEXT', 69 | 'default' => '', 70 | 'value' => '', 71 | 'validators' => array( 72 | 0 => array( 73 | 'type' => 'REGEX', 74 | 'regex' => '/^[0-9a-f]{48}$/', 75 | 'errmsg' => 'token_error_regex' 76 | ), 77 | ), 78 | 'width' => '48', 79 | 'maxlength' => '48' 80 | ), 81 | 'allowed_zones' => array( 82 | 'datatype' => 'VARCHAR', 83 | 'formtype' => 'MULTIPLE', 84 | 'separator' => ',', 85 | 'default' => '', 86 | 'validators' => array( 87 | 0 => array( 88 | 'type' => 'NOTEMPTY', 89 | 'errmsg' => 'allowed_zones_notempty_txt' 90 | ) 91 | ), 92 | 'datasource' => array( 93 | 'type' => 'CUSTOM', 94 | 'class' => 'ddns_custom_datasource', 95 | 'function' => 'dns_zones' 96 | ), 97 | 'value' => '', 98 | 'name' => 'zones', 99 | 'maxlength' => '500' 100 | ), 101 | 'allowed_record_types' => array( 102 | 'datatype' => 'VARCHAR', 103 | 'formtype' => 'CHECKBOXARRAY', 104 | 'separator' => ',', 105 | 'default' => 'A,AAAA', 106 | 'validators' => array( 107 | 0 => array( 108 | 'type' => 'NOTEMPTY', 109 | 'errmsg' => 'allowed_record_types_notempty_txt' 110 | ) 111 | ), 112 | 'value' => array('A' => 'A (IPv4)', 'AAAA' => 'AAAA (IPv6)', 'TXT' => 'TXT'), 113 | 'name' => 'record_types', 114 | 'maxlength' => '255' 115 | ), 116 | 'limit_records' => array( 117 | 'datatype' => 'VARCHAR', 118 | 'formtype' => 'TEXT', 119 | 'validators' => array( 120 | 0 => array( 121 | 'type' => 'REGEX', 122 | 'regex' => '/^([a-zA-Z0-9\_\.\-\*],?)*$/', 123 | 'errmsg' => 'limit_records_error_regex_txt' 124 | ) 125 | ), 126 | 'filters' => array( 127 | 0 => array('event' => 'SAVE', 'type' => 'IDNTOASCII'), 128 | 1 => array('event' => 'SHOW', 'type' => 'IDNTOUTF8'), 129 | 2 => array('event' => 'SAVE', 'type' => 'TOLOWER') 130 | ), 131 | 'default' => '', 132 | 'value' => '', 133 | 'width' => '30', 134 | 'maxlength' => '255' 135 | ), 136 | 'active' => array( 137 | 'datatype' => 'VARCHAR', 138 | 'formtype' => 'CHECKBOX', 139 | 'default' => 'Y', 140 | 'value' => array(0 => 'N', 1 => 'Y') 141 | ), 142 | 143 | //*** END Datatable columns ********************************** 144 | ) 145 | ); 146 | ?> 147 | -------------------------------------------------------------------------------- /lib/admin.conf.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /lib/classes/ddns_custom_datasource.inc.php: -------------------------------------------------------------------------------- 1 | db->queryAllRecords("SELECT id,origin FROM dns_soa WHERE ".$app->tform->getAuthSQL('r')." ORDER BY origin"); 8 | $zones_new = array(); 9 | foreach($zones as $zone) { 10 | $zones_new[$zone['origin']] = $zone['origin']; 11 | } 12 | return $zones_new; 13 | } 14 | 15 | } 16 | 17 | ?> 18 | -------------------------------------------------------------------------------- /lib/ddns.menu.php: -------------------------------------------------------------------------------- 1 | "Tokens", 9 | 'target' => 'content', 10 | 'link' => 'ddns/ddns_token_list.php', 11 | 'html_id' => 'dns_wizard'); 12 | $module['nav'][] = array( 13 | 'title' => 'Dynamic DNS', 14 | 'open' => 1, 15 | 'items' => $items 16 | ); 17 | 18 | ?> 19 | 20 | -------------------------------------------------------------------------------- /lib/lang/de_ddns_token.lng: -------------------------------------------------------------------------------- 1 | _ispconfig = $ispconfig; 20 | if ($this->_ispconfig->is_under_maintenance()) { 21 | $this->_response_writer->maintenance(); 22 | exit; 23 | } 24 | if (isset($_SERVER['HTTP_X_ORIGINAL_REQUEST_URI'])) { 25 | $request_uri = parse_url($_SERVER['HTTP_X_ORIGINAL_REQUEST_URI'], PHP_URL_PATH); 26 | } else { 27 | $request_uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); 28 | } 29 | $this->_remote_ip = $this->getRequestIp($config); 30 | switch ($request_uri) { 31 | case '/nic/dyndns': 32 | case '/nic/statdns': 33 | # DynDns 1 endpoints 34 | require_once(dirname(__FILE__) . '/request/DynDnsRequest.php'); 35 | require_once(dirname(__FILE__) . '/response/DynDns1ResponseWriter.php'); 36 | $this->_response_writer = new DynDns1ResponseWriter($ispconfig); 37 | $this->_requests[] = new DynDnsRequest($_GET['host_id']); 38 | break; 39 | case '/nic/update': 40 | # DynDns 2 endpoint 41 | require_once(dirname(__FILE__) . '/request/DynDnsRequest.php'); 42 | require_once(dirname(__FILE__) . '/response/DynDns2ResponseWriter.php'); 43 | $this->_response_writer = new DynDns2ResponseWriter(); 44 | $hostnames = explode(',', $_GET['hostname']); 45 | if (sizeof($hostnames) == 0) { 46 | $this->_response_writer->missingInput(new DynDnsRequest(null)); 47 | exit; 48 | } 49 | foreach ($hostnames as $hostname) { 50 | $this->_requests[] = new DynDnsRequest($hostname); 51 | } 52 | break; 53 | default: 54 | require_once(dirname(__FILE__) . '/request/DefaultDdnsRequest.php'); 55 | require_once(dirname(__FILE__) . '/response/DefaultDdnsResponseWriter.php'); 56 | $this->_response_writer = new DefaultDdnsResponseWriter($ispconfig); 57 | $this->_requests[] = new DefaultDdnsRequest(); 58 | } 59 | $this->_token = new DdnsToken($ispconfig, $this->_remote_ip, $this->getTokenFromRequest(), $this->_response_writer); 60 | } 61 | 62 | public function getRequestIp($config): string 63 | { 64 | $remote_ip = $_SERVER['REMOTE_ADDR']; 65 | if (!isset($_SERVER["HTTP_{$config['PROXY_KEY_HEADER']}"]) || !isset($_SERVER["HTTP_{$config['PROXY_IP_HEADER']}"])) { 66 | return $remote_ip; 67 | } 68 | if ($remote_ip !== $config['TRUSTED_PROXY_IP']) { 69 | header("HTTP/1.1 500 Internal Server Error"); 70 | echo "Untrusted proxy: '$remote_ip' does not match config TRUSTED_PROXY_IP.\n"; 71 | exit; 72 | } 73 | if (empty($config['TRUSTED_PROXY_KEY']) || $_SERVER["HTTP_{$config['PROXY_KEY_HEADER']}"] !== $config['TRUSTED_PROXY_KEY']) { 74 | header("HTTP/1.1 500 Internal Server Error"); 75 | echo "Proxy key is invalid.\n"; 76 | exit; 77 | } 78 | $forwarded_ip = $_SERVER["HTTP_{$config['PROXY_IP_HEADER']}"]; 79 | if (filter_var($forwarded_ip, FILTER_VALIDATE_IP) === false) { 80 | header("HTTP/1.1 500 Internal Server Error"); 81 | echo "The proxy has forwarded an invalid IP: '$forwarded_ip'.\n"; 82 | exit; 83 | } 84 | return $forwarded_ip; 85 | } 86 | 87 | protected function getTokenFromRequest(): ?string 88 | { 89 | if (isset($_GET['token'])) { 90 | $token = $_GET['token']; 91 | } else if (isset($_SERVER['PHP_AUTH_PW'])) { 92 | $token = $_SERVER['PHP_AUTH_PW']; 93 | } else { 94 | return null; 95 | } 96 | // only hex characters allowed in token 97 | return preg_replace("/[^0-9^a-f]/", "", $token); 98 | } 99 | 100 | public function process(): void 101 | { 102 | $records = []; 103 | foreach ($this->_requests as $request) { 104 | $request->autoSetMissingInput($this->_token, $this->_remote_ip); 105 | $request->validate($this->_token, $this->_response_writer, $this->_ispconfig); 106 | $records[] = $this->loadDnsRecord($request); 107 | } 108 | $this->updateDnsRecords($records); 109 | } 110 | 111 | protected function loadDnsRecord(DdnsRequest $request): array 112 | { 113 | // try to load zone 114 | $soa = $this->_ispconfig->db->queryOneRecord( 115 | "SELECT id,server_id,sys_userid,sys_groupid,origin,ttl,serial FROM dns_soa WHERE origin=?", 116 | $request->getZone() 117 | ); 118 | if ($soa == null || $soa['id'] == null) { 119 | $this->_response_writer->dnsNotFound("zone '{$request->getZone()}'"); 120 | exit; 121 | } 122 | 123 | // try to load record (for update/delete) 124 | $rr = null; 125 | $rrResult = $this->_ispconfig->db->query( 126 | "SELECT id,data,ttl,serial FROM dns_rr WHERE type=? AND name=? AND zone=?", 127 | $request->getRecordType(), 128 | $request->getRecord(), 129 | $soa['id'] 130 | ); 131 | if ($rrResult && $rrResult->rows() > 0) { 132 | if ($request->getAction() === 'update') { 133 | // because we do not work with IDs or 'oldData' (yet), there must be exactly one existing record for update operations 134 | if ($rrResult->rows() > 1) { 135 | $rrResult->free(); 136 | $this->_response_writer->internalError("Found more than one record to update, unable to proceed"); 137 | exit; 138 | } 139 | $rr = $rrResult->get(); 140 | } else if ($request->getData() !== null) { 141 | // for add / delete, check matching record by data 142 | while($record = $rrResult->get()) { 143 | if ($record['data'] === $request->getData()) { 144 | $rr = $record; 145 | break; 146 | } 147 | } 148 | } 149 | $rrResult->free(); 150 | } 151 | if ($request->getAction() !== 'add' && $rr === null) { 152 | $this->_response_writer->dnsNotFound( 153 | "record '{$request->getRecord()}' of type '{$request->getRecordType()}' in zone '{$request->getZone()}'" 154 | ); 155 | exit; 156 | } 157 | if ($request->getAction() === 'add' && $rr !== null) { 158 | $this->_response_writer->noUpdateRequired($request); 159 | exit; 160 | } 161 | 162 | return [ 163 | 'request' => $request, 164 | 'soa' => $soa, 165 | 'rr' => $rr 166 | ]; 167 | } 168 | 169 | protected function updateDnsRecords(array $records): void 170 | { 171 | $update_performed = false; 172 | $unique_soa = []; 173 | $longest_ttl = 0; 174 | // update DNS records 175 | foreach ($records as $record) { 176 | $request = $record['request']; 177 | $soa = $record['soa']; 178 | $rr = $record['rr']; 179 | if ($request->getAction() === 'delete') { 180 | // delete record 181 | if ($rr === null) { 182 | // cannot delete non-existing record 183 | continue; 184 | } 185 | $this->_ispconfig->db->datalogDelete('dns_rr', 'id', $rr['id']); 186 | $update_performed = true; 187 | if ($longest_ttl < (int)$rr['ttl']) { 188 | $longest_ttl = (int)$rr['ttl']; 189 | } 190 | } else if ($request->getAction() === 'update') { 191 | // update record 192 | if ($rr === null) { 193 | $this->_response_writer->internalError("Record is missing for action update, unable to proceed"); 194 | exit; 195 | } 196 | // check if update is required 197 | if ($rr['data'] === $request->getData()) { 198 | continue; 199 | } 200 | // Update the RR 201 | $rr_update = array( 202 | "data" => $request->getData(), 203 | "serial" => $this->_ispconfig->validate_dns->increase_serial($rr["serial"]), 204 | "stamp" => date('Y-m-d H:i:s') 205 | ); 206 | $this->_ispconfig->db->datalogUpdate('dns_rr', $rr_update, 'id', $rr['id']); 207 | $update_performed = true; 208 | if ($longest_ttl < (int)$rr['ttl']) { 209 | $longest_ttl = (int)$rr['ttl']; 210 | } 211 | } else if ($request->getAction() === 'add') { 212 | // check record with same data was already found 213 | if ($rr !== null && $rr['data'] === $request->getData()) { 214 | continue; 215 | } 216 | // create record 217 | // Get the limits of the client 218 | $client_group_id = intval($soa["sys_groupid"]); 219 | $client = $this->_ispconfig->db->queryOneRecord( 220 | "SELECT limit_dns_record FROM sys_group, client WHERE sys_group.client_id = client.client_id and sys_group.groupid = ?", 221 | $client_group_id 222 | ); 223 | // Check if the user may add another record. 224 | if($client["limit_dns_record"] >= 0) { 225 | $tmp = $this->_ispconfig->db->queryOneRecord( 226 | "SELECT count(id) as number FROM dns_rr WHERE sys_groupid = ?", 227 | $client_group_id 228 | ); 229 | if($tmp["number"] >= $client["limit_dns_record"]) { 230 | $this->_response_writer->forbidden("new record. dns record limit reached."); 231 | exit; 232 | } 233 | } 234 | $rr_insert = array( 235 | // "id" auto-generated 236 | "server_id" => $soa["server_id"], 237 | "sys_userid" => $soa["sys_userid"], 238 | "sys_groupid" => $soa["sys_groupid"], 239 | "sys_perm_user" => 'riud', 240 | "sys_perm_group" => 'riud', 241 | "zone" => $soa['id'], 242 | "type" => $request->getRecordType(), 243 | "ttl" => '3600', 244 | "name" => $request->getRecord(), 245 | "data" => $request->getData(), 246 | "serial" => $this->_ispconfig->validate_dns->increase_serial($rr["serial"]), 247 | "active" => 'Y', 248 | "stamp" => date('Y-m-d H:i:s') 249 | ); 250 | // insert the RR 251 | $this->_ispconfig->db->datalogInsert('dns_rr', $rr_insert, 'id'); 252 | $update_performed = true; 253 | if ($longest_ttl < (int)$soa['ttl']) { 254 | $longest_ttl = (int)$soa['ttl']; 255 | } 256 | } 257 | if (!array_key_exists($soa['id'], $unique_soa)) { 258 | $unique_soa[$soa['id']] = $soa; 259 | } 260 | } 261 | 262 | if (!$update_performed) { 263 | $this->_response_writer->noUpdateRequired($records[0]['request']); 264 | exit; 265 | } 266 | 267 | // Update the serial number of the affected SOA records 268 | foreach ($unique_soa as $soa) { 269 | //* Update the serial number of the SOA record 270 | $soa_update = array( 271 | "serial" => $this->_ispconfig->validate_dns->increase_serial($soa["serial"]) 272 | ); 273 | $this->_ispconfig->db->datalogUpdate('dns_soa', $soa_update, 'id', $soa['id']); 274 | 275 | // cron runs every full minute, calculate seconds left 276 | $cron_eta = 60 - date('s'); 277 | } 278 | $this->_response_writer->successfulUpdate($records[0]['request'], $longest_ttl, $cron_eta); 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /lib/updater/request/DdnsRequest.php: -------------------------------------------------------------------------------- 1 | _zone = $zone; 20 | } 21 | 22 | public function getZone(): ?string 23 | { 24 | return $this->_zone; 25 | } 26 | 27 | public function setRecord($record): void 28 | { 29 | $this->_record = $record; 30 | } 31 | 32 | public function getRecord(): ?string 33 | { 34 | return $this->_record; 35 | } 36 | 37 | public function setRecordType($record_type): void 38 | { 39 | $this->_record_type = $record_type; 40 | } 41 | 42 | public function getRecordType(): ?string 43 | { 44 | return $this->_record_type; 45 | } 46 | 47 | public function setData($data): void 48 | { 49 | $this->_data = $data; 50 | } 51 | 52 | public function getData(): ?string 53 | { 54 | return $this->_data; 55 | } 56 | 57 | public function setAction($action): void 58 | { 59 | // ignore invalid actions... 60 | if (in_array($action, array('add', 'delete', 'update'), true)) { 61 | $this->_action = $action; 62 | } 63 | } 64 | 65 | public function getAction(): ?string 66 | { 67 | return $this->_action; 68 | } 69 | 70 | abstract public function autoSetMissingInput(DdnsToken $token, string $remote_ip): void; 71 | 72 | // match zone and record from a hostname 73 | protected function match_from_hostname(string $hostname, DdnsToken $token): void 74 | { 75 | $hostname = rtrim($hostname, '.'); 76 | 77 | // match hostname with allowed dns zones 78 | $matching_zones = []; 79 | foreach ($token->getAllowedZones() as $allowed_zone) { 80 | if(strpos($hostname, rtrim($allowed_zone, '.')) !== false) { 81 | $matching_zones[] = $allowed_zone; 82 | } 83 | } 84 | if (empty($matching_zones)) { 85 | return; 86 | } else if (sizeof($matching_zones) === 1) { 87 | $this->setZone($matching_zones[0]); 88 | } else { 89 | $closest_match = ''; 90 | foreach ($matching_zones as $matching_zone) { 91 | if(sizeof($matching_zone) > sizeof($closest_match)) { 92 | $closest_match = $matching_zone; 93 | } 94 | } 95 | $this->setZone($closest_match); 96 | } 97 | 98 | // match records with allowed records 99 | $zone_length = strlen(rtrim($this->getZone(), '.')); 100 | if ($zone_length < strlen($hostname)) { 101 | $record = substr($hostname, 0, - $zone_length - 1); 102 | $this->setRecord($record); 103 | } else if (count($token->getLimitRecords()) == 0) { 104 | $this->setRecord(''); 105 | } 106 | } 107 | 108 | public function validate(DdnsToken $token, DdnsResponseWriter $response_writer, app $app): void 109 | { 110 | // check if requested zone is allowed (allowed_zones must be set) 111 | if ($this->getZone() !== null && !in_array($this->getZone(), $token->getAllowedZones(), true)) { 112 | $response_writer->forbidden("zone {$this->getZone()}"); 113 | exit; 114 | } 115 | 116 | // check if record restriction is set and requested zone is allowed 117 | if ($this->getRecord() !== null && count($token->getLimitRecords()) !== 0 && !in_array($this->getRecord(), $token->getLimitRecords(), true)) { 118 | $response_writer->forbidden("record {$this->getRecord()}"); 119 | exit; 120 | } 121 | 122 | // check if requested type is allowed (allowed_record_types must be set) 123 | if ($this->getRecordType() !== null && !in_array($this->getRecordType(), $token->getAllowedRecordTypes(), true)) { 124 | $response_writer->forbidden("record type {$this->getRecordType()}"); 125 | exit; 126 | } 127 | 128 | // check if all required data is available 129 | if ($this->getZone() === null || $this->getRecord() === null || $this->getRecordType() === null || $this->getData() === null) { 130 | $response_writer->missingInput($this); 131 | exit; 132 | } 133 | 134 | // validate data for given type 135 | if ($this->getRecordType() === 'A') { 136 | $ip = filter_var($this->getData(), FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); 137 | if (!$ip) { 138 | $response_writer->invalidIpAddress($this->getData()); 139 | exit; 140 | } 141 | // write back filtered ip 142 | $this->setData($ip); 143 | } else if ($this->getRecordType() === 'AAAA') { 144 | $ip = filter_var($this->getData(), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); 145 | if (!$ip) { 146 | $response_writer->invalidIpAddress($this->getData()); 147 | exit; 148 | } 149 | // write back filtered ip 150 | $this->setData($ip); 151 | } else if ($this->getRecordType() === 'TXT') { 152 | // IDNTOASCII and TOLOWER transformations for record name 153 | $record = $app->functions->idn_encode($this->getRecord()); 154 | $record = strtolower($record); 155 | $this->setRecord($record); 156 | 157 | // validation for data 158 | if ($this->getData() === '') { 159 | $response_writer->missingInput($this); 160 | exit; 161 | } else if (strlen($this->getData()) > 255) { 162 | $response_writer->invalidData("maximum 255 characters"); 163 | exit; 164 | } 165 | 166 | if ($this->getAction() === 'update') { 167 | $response_writer->forbidden("TXT update"); 168 | } 169 | } else { 170 | $response_writer->forbidden('record type ' . $this->getRecordType()); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /lib/updater/request/DefaultDdnsRequest.php: -------------------------------------------------------------------------------- 1 | setZone($zone); 12 | } 13 | $this->setRecord($_GET['record'] ?? $_POST['record']); 14 | $this->setRecordType($_GET['type'] ?? $_POST['type']); 15 | $this->setData($_GET['data'] ?? $_POST['data']); 16 | $this->setAction($_GET['action'] ?? $_POST['action']); 17 | } 18 | 19 | public function autoSetMissingInput(DdnsToken $token, string $remote_ip): void 20 | { 21 | // auto-set zone based on record if possible 22 | if ($this->getZone() === null && $this->getRecord() !== null && $this->getRecord() !== '') { 23 | parent::match_from_hostname($this->getRecord(), $token); 24 | } 25 | 26 | // auto-set zone if possible 27 | if ($this->getZone() === null && count($token->getAllowedZones()) === 1) { 28 | $this->setZone($token->getAllowedZones()[0]); 29 | } 30 | // auto-set record if possible 31 | if ($this->getRecord() === null && count($token->getLimitRecords()) === 1) { 32 | $this->setRecord($token->getLimitRecords()[0]); 33 | } else if ($this->getRecord() === null && count($token->getLimitRecords()) === 0) { 34 | $this->setRecord(''); 35 | } 36 | 37 | // auto-set type if possible 38 | if ($this->getRecordType() === null && count($token->getAllowedRecordTypes()) === 1) { 39 | $this->setRecordType($token->getAllowedRecordTypes()[0]); 40 | } 41 | 42 | // auto-set data if possible 43 | if ($this->getData() === null && ($this->getRecordType() === null || $this->getRecordType() === 'A' || $this->getRecordType() === 'AAAA')) { 44 | $this->setData($remote_ip); 45 | // auto-set type based on IP 46 | if ($this->getRecordType() === null && $this->getData() !== null && filter_var($this->getData(), FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { 47 | $this->setRecordType('A'); 48 | } else if ($this->getRecordType() === null && $this->getData() !== null && filter_var($this->getData(), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { 49 | $this->setRecordType('AAAA'); 50 | } 51 | } 52 | 53 | // auto-set action if provided 54 | if ($this->getAction() === null) { 55 | switch ($_SERVER['REQUEST_METHOD']) { 56 | case 'DELETE': 57 | $this->setAction('delete'); 58 | break; 59 | case 'POST': 60 | $this->setAction('add'); 61 | break; 62 | default: 63 | // GET, PUT, PATCH, ... 64 | $this->setAction('update'); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/updater/request/DynDnsRequest.php: -------------------------------------------------------------------------------- 1 | _hostname = $hostname; 12 | $this->setData($_GET['myip']); 13 | // action is always update (for DynDNS requests) 14 | $this->setAction('update'); 15 | // zone, record and type cannot be determined from http request only 16 | } 17 | 18 | public function autoSetMissingInput(DdnsToken $token, string $remote_ip): void 19 | { 20 | if ($this->_hostname !== null) { 21 | parent::match_from_hostname($this->_hostname, $token); 22 | } 23 | 24 | if ($this->getData() === null) { 25 | $this->setData($remote_ip); 26 | } 27 | 28 | // auto-set type 29 | if ($this->getData() !== null && filter_var($this->getData(), FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { 30 | $this->setRecordType('A'); 31 | } else if ($this->getData() !== null && filter_var($this->getData(), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { 32 | $this->setRecordType('AAAA'); 33 | } 34 | } 35 | 36 | public function validate(DdnsToken $token, DdnsResponseWriter $response_writer, app $app): void 37 | { 38 | if ($this->_hostname === null) { 39 | $response_writer->missingInput($this); 40 | exit; 41 | } 42 | // check if all required data is available 43 | if ($this->getZone() === null || $this->getRecord() === null) { 44 | $response_writer->dnsNotFound($this->_hostname); 45 | exit; 46 | } else if ($this->getRecordType() !== 'A' && $this->getRecordType() !== 'AAAA') { 47 | $response_writer->invalidIpAddress($this->getData()); 48 | } 49 | parent::validate($token, $response_writer, $app); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/updater/response/DdnsResponseWriter.php: -------------------------------------------------------------------------------- 1 | _ispconfig = $ispconfig; 12 | } 13 | 14 | public function invalidOrMissingToken(): void 15 | { 16 | header("HTTP/1.1 401 Unauthorized"); 17 | echo "Missing or invalid token.\n"; 18 | exit; 19 | } 20 | 21 | public function maintenance(): void 22 | { 23 | header("HTTP/1.1 500 Internal Server Error"); 24 | echo "This ISPConfig installation is currently under maintenance. We should be back shortly. Thank you for your patience.\n"; 25 | exit; 26 | } 27 | 28 | public function tooManyLoginAttempts(): void 29 | { 30 | header("HTTP/1.1 429 Too Many Requests"); 31 | echo $this->_ispconfig->lng('error_user_too_many_logins')."\n"; 32 | exit; 33 | } 34 | 35 | public function forbidden(string $entity): void 36 | { 37 | header("HTTP/1.1 403 Forbidden"); 38 | echo "Permission denied for $entity\n"; 39 | exit; 40 | } 41 | 42 | public function missingInput(DdnsRequest $request): void 43 | { 44 | header("HTTP/1.1 400 Bad Request"); 45 | echo "Missing input data, zone={$request->getZone()}, record={$request->getRecord()}, type={$request->getRecordType()}, data={$request->getData()}.\n"; 46 | exit; 47 | } 48 | 49 | public function invalidIpAddress(?string $ip): void 50 | { 51 | header("HTTP/1.1 400 Bad Request"); 52 | echo "Invalid IP address: $ip\n"; 53 | exit; 54 | } 55 | 56 | public function invalidData(string $reason): void 57 | { 58 | header("HTTP/1.1 400 Bad Request"); 59 | echo "Invalid Data: $reason\n"; 60 | exit; 61 | } 62 | 63 | public function dnsNotFound(string $dns): void 64 | { 65 | header("HTTP/1.1 404 Not Found"); 66 | echo "Could not find $dns\n"; 67 | exit; 68 | } 69 | 70 | public function internalError(string $message): void 71 | { 72 | header("HTTP/1.1 500 Internal Server Error"); 73 | echo "$message.\n"; 74 | exit; 75 | } 76 | 77 | public function noUpdateRequired(DdnsRequest $request): void 78 | { 79 | // return normal 200, no http error code 80 | if ($request->getAction() === 'delete') { 81 | echo "ERROR: {$request->getRecord()} does not exit.\n"; 82 | } else { 83 | echo "ERROR: {$request->getData()} is already set in {$request->getRecord()}.\n"; 84 | } 85 | exit; 86 | } 87 | 88 | public function successfulUpdate(DdnsRequest $request, int $record_ttl, int $cron_eta): void 89 | { 90 | if ($request->getAction() === 'delete') { 91 | echo "Scheduled delete of record {$request->getRecord()}. Schedule runs in $cron_eta seconds. Record TTL: $record_ttl.\n"; 92 | } else { 93 | echo "Scheduled update to '{$request->getData()}' of record {$request->getRecord()}. Schedule runs in $cron_eta seconds. Record TTL: $record_ttl.\n"; 94 | } 95 | exit; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /lib/updater/response/DynDns1ResponseWriter.php: -------------------------------------------------------------------------------- 1 | _ispconfig = $ispconfig; 12 | } 13 | private function exit(): void { 14 | // current ddclient implementation does not handle chunked encoding, setting content-length fixes this 15 | // see: https://github.com/ddclient/ddclient/issues/499#issuecomment-1447465250 16 | header('Content-Length: ' . ob_get_length()); 17 | exit; 18 | } 19 | 20 | private function dynDns1Error(string $message) { 21 | echo "
{tmpl_var name="toolsarea_head_txt"}
25 | 26 | 27 | 28 | 29 |{tmpl_var name='search_limit'} | 43 |||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
46 | | 48 | | 50 | | 51 | | 52 | | 53 | | 54 | 55 | | 56 |||||||
{tmpl_var name="active"} | 62 |{tmpl_var name="sys_groupid"} | 64 |{tmpl_var name="token"} | 66 |{tmpl_var name="allowed_zones"} | 67 |{tmpl_var name="allowed_record_types"} | 68 |{tmpl_var name="limit_records"} | 69 |70 | 71 | 72 | | 73 |||||||
{tmpl_var name='globalsearch_noresults_text_txt'} | 79 |{tmpl_var name='globalsearch_noresults_text_txt'} | 82 ||||||||||||