diff --git a/src/etc/inc/dyndns.class b/src/etc/inc/dyndns.class index f2515b4dbb..f461999c23 100644 --- a/src/etc/inc/dyndns.class +++ b/src/etc/inc/dyndns.class @@ -245,6 +245,8 @@ if (!$dnsTTL) $this->_error(9); break; case 'digitalocean': + case 'linode': + case 'linode-v6': if (!$dnsPass) $this->_error(4); if (!$dnsHost) $this->_error(5); if (!$dnsDomain) $this->_error(5); @@ -279,6 +281,7 @@ case 'dreamhost-v6': case 'godaddy-v6': case 'azurev6': + case 'linode-v6': $this->_useIPv6 = true; break; default: @@ -371,6 +374,8 @@ case 'godaddy-v6': case 'azure': case 'azurev6': + case 'linode': + case 'linode-v6': $this->_update(); if ($this->_dnsDummyUpdateDone == true) { // If a dummy update was needed, then sleep a while and do the update again to put the proper address back. @@ -1117,7 +1122,92 @@ curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); break; + case 'linode': + case 'linode-v6': + $linode_api = "https://api.linode.com/v4"; + $record_type = $this->_useIPv6 ? "AAAA" : "A"; + $record_name = $this->_dnsHost == "@" ? "" : $this->_dnsHost; + $err_prefix = gettext("Dynamic DNS") . " {$this->_dnsService} ({$this->_FQDN}): (" . gettext("Error") . ")"; + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + 'Accept: application/json', + 'Content-Type: application/json', + 'Authorization: Bearer ' . $this->_dnsPass + )); + // get domain id + curl_setopt($ch, CURLOPT_URL, "$linode_api/domains"); + $domains_output = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ( $http_code == 401 && preg_match('/invalid oauth token/i', $domains_output) ) { + log_error("$err_prefix " . gettext("querying domains") . ": " . gettext("User Authentication Failed")); + return false; + } else if ( $http_code == 401 ) { + log_error("$err_prefix " . gettext("querying domains") . ": " . gettext("User Authorization Failed")); + return false; + } else if ( $http_code != 200 ) { + log_error("$err_prefix " . gettext("querying domains") . ": " . + ( isset($domains_result["errors"][0]["reason"]) ? $domains_result["errors"][0]["reason"] : "HTTP $http_code" ) ); + return false; + } + + $domains_result = json_decode($domains_output, TRUE); + foreach($domains_result["data"] as $domains_entry) { + if ($domains_entry["domain"] == $this->_dnsDomain) { + $domain_id = $domains_entry["id"]; + } + } + if ( ! $domain_id ) { + log_error("$err_prefix " . gettext("no domain ID for domain") . ": '{$this->_dnsDomain}'"); + return false; + } + if ($this->_dnsVerboseLog) { + log_error("_update(): " . sprintf(gettext("found domain id: %s"), $domain_id)); + } + + // get existing record if present + curl_setopt($ch, CURLOPT_URL, "$linode_api/domains/$domain_id/records"); + $records_output = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ( $http_code != 200 ) + { + log_error("$err_prefix " . gettext("querying domains") . ": " . + ( isset($domains_result["errors"][0]["reason"]) ? $domains_result["errors"][0]["reason"] : "HTTP $http_code" ) ); + return false; + } + + $records_result = json_decode($records_output, TRUE); + foreach($records_result["data"] as $records_entry) { + if ( $records_entry["type"] == $record_type && $records_entry["name"] == $record_name ) { + // not adding support for pagination at this time, hope you have < 100 records! + $record = $records_entry; + } + } + if ($this->_dnsVerboseLog) { + log_error("_update(): " . ( $record ? sprintf(gettext("found existing record id: %s"), $record["id"]) : gettext("no matching record found") )); + } + + if (is_array($record)) { + // update existing record + $record["target"] = $this->_dnsIP; + $record["ttl_sec"] = (int) $this->_dnsTTL; // linode may round this up, 0 = zone default + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($record)); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); + curl_setopt($ch, CURLOPT_URL, "$linode_api/domains/$domain_id/records/" . $record["id"]); + } else { + // create a new record + $record = array( + "type" => $record_type, + "name" => $record_name, + "target" => $this->_dnsIP, + "ttl_sec" => (int) $this->_dnsTTL, // linode may round this up, 0 = zone default + ); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($record)); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($ch, CURLOPT_URL, "$linode_api/domains/$domain_id/records"); + } + + break; default: break; } @@ -2103,6 +2193,24 @@ $this->_debug($data); } break; + case 'linode': + case 'linode-v6': + $status_intro = gettext("Dynamic DNS") . " {$this->_dnsService} ({$this->_FQDN}): "; + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $result = json_decode($data,true); + if ($this->_dnsVerboseLog) { + log_error(sprintf(gettext('_checkStatus() results: %1$s'), $data)); + } + if ($http_code == 200 && isset($result["id"]) && ! isset($result["errors"])) { + $status = $status_intro . $success_str . gettext("IP Address Changed Successfully!"); + $successful_update = true; + } else if ( $http_code == 401 && preg_match('/not authorized to use/i', $data) ) { + $status = $status_intro . $error_str . gettext("User Authorization Failed"); + } else { + $status = $status_intro . $error_str . + ( isset($domains_result["errors"][0]["reason"]) ? $domains_result["errors"][0]["reason"] : "HTTP $http_code" ); + } + break; default: break; } diff --git a/src/etc/inc/globals.inc b/src/etc/inc/globals.inc index 39017f591c..f4e4b6f5d7 100644 --- a/src/etc/inc/globals.inc +++ b/src/etc/inc/globals.inc @@ -209,7 +209,7 @@ $factory_default_checkipservice = array( "descr" => 'Default Check IP Service' ); -$dyndns_split_domain_types = array("namecheap", "cloudflare", "cloudflare-v6", "gratisdns", "cloudns", "godaddy", "godaddy-v6"); +$dyndns_split_domain_types = array("namecheap", "cloudflare", "cloudflare-v6", "gratisdns", "cloudns", "godaddy", "godaddy-v6", "linode", "linode-v6"); /* pf tokens from FreeBSD source sbin/pfctl/parse.y (plus our custom entries at the end)*/ global $pf_reserved_keywords; diff --git a/src/etc/inc/services.inc b/src/etc/inc/services.inc index 894a19e73f..f70a32f146 100644 --- a/src/etc/inc/services.inc +++ b/src/etc/inc/services.inc @@ -24,8 +24,8 @@ */ -define('DYNDNS_PROVIDER_VALUES', 'all-inkl azure azurev6 citynetwork cloudflare cloudflare-v6 cloudns custom custom-v6 digitalocean dnsexit dnsimple dnsmadeeasy dnsomatic dreamhost dreamhost-v6 duiadns duiadns-v6 dyndns dyndns-custom dyndns-static dyns easydns eurodns freedns freedns-v6 glesys godaddy godaddy-v6 googledomains gratisdns he-net he-net-v6 he-net-tunnelbroker hover loopia namecheap noip noip-free ods opendns ovh-dynhost route53 route53-v6 selfhost spdyn spdyn-v6 zoneedit'); -define('DYNDNS_PROVIDER_DESCRIPTIONS', 'All-Inkl.com,Azure DNS,Azure DNS (v6),City Network,Cloudflare,Cloudflare (v6),ClouDNS,Custom,Custom (v6),DigitalOcean,DNSexit,DNSimple,DNS Made Easy,DNS-O-Matic,DreamHost,Dreamhost (v6),DuiaDns.net,DuiaDns.net (v6),DynDNS (dynamic),DynDNS (custom),DynDNS (static),DyNS,easyDNS,Euro Dns,freeDNS,freeDNS (v6),GleSYS,GoDaddy,GoDaddy (v6),Google Domains,GratisDNS,HE.net,HE.net (v6),HE.net Tunnelbroker,Hover,Loopia,Namecheap,No-IP,No-IP (free),ODS.org,OpenDNS,OVH DynHOST,Route 53,Route 53 (v6),SelfHost,SPDYN,SPDYN (v6),ZoneEdit'); +define('DYNDNS_PROVIDER_VALUES', 'all-inkl azure azurev6 citynetwork cloudflare cloudflare-v6 cloudns custom custom-v6 digitalocean dnsexit dnsimple dnsmadeeasy dnsomatic dreamhost dreamhost-v6 duiadns duiadns-v6 dyndns dyndns-custom dyndns-static dyns easydns eurodns freedns freedns-v6 glesys godaddy godaddy-v6 googledomains gratisdns he-net he-net-v6 he-net-tunnelbroker hover linode linode-v6 loopia namecheap noip noip-free ods opendns ovh-dynhost route53 route53-v6 selfhost spdyn spdyn-v6 zoneedit'); +define('DYNDNS_PROVIDER_DESCRIPTIONS', 'All-Inkl.com,Azure DNS,Azure DNS (v6),City Network,Cloudflare,Cloudflare (v6),ClouDNS,Custom,Custom (v6),DigitalOcean,DNSexit,DNSimple,DNS Made Easy,DNS-O-Matic,DreamHost,Dreamhost (v6),DuiaDns.net,DuiaDns.net (v6),DynDNS (dynamic),DynDNS (custom),DynDNS (static),DyNS,easyDNS,Euro Dns,freeDNS,freeDNS (v6),GleSYS,GoDaddy,GoDaddy (v6),Google Domains,GratisDNS,HE.net,HE.net (v6),HE.net Tunnelbroker,Hover,Linode,Linode (v6),Loopia,Namecheap,No-IP,No-IP (free),ODS.org,OpenDNS,OVH DynHOST,Route 53,Route 53 (v6),SelfHost,SPDYN,SPDYN (v6),ZoneEdit'); /* implement ipv6 route advertising daemon */ function services_radvd_configure($blacklist = array()) { diff --git a/src/usr/local/www/services_dyndns_edit.php b/src/usr/local/www/services_dyndns_edit.php index 7e0a47f380..eb5eaa972c 100644 --- a/src/usr/local/www/services_dyndns_edit.php +++ b/src/usr/local/www/services_dyndns_edit.php @@ -73,7 +73,7 @@ if ($_POST['save'] || $_POST['force']) { unset($input_errors); $pconfig = $_POST; - if (($pconfig['type'] == "freedns" || $pconfig['type'] == "freedns-v6" || $pconfig['type'] == "namecheap" || $pconfig['type'] == "digitalocean") && $_POST['username'] == "") { + if (($pconfig['type'] == "freedns" || $pconfig['type'] == "freedns-v6" || $pconfig['type'] == "namecheap" || $pconfig['type'] == "digitalocean" || $pconfig['type'] == "linode" || $pconfig['type'] == "linode-v6") && $_POST['username'] == "") { $_POST['username'] = "none"; } @@ -106,6 +106,7 @@ if ($_POST['save'] || $_POST['force']) { } if (isset($_POST['host']) && in_array("host", $reqdfields)) { + $allow_wildcard = false; /* Namecheap can have a @. and *. in hostname */ if ($pconfig['type'] == "namecheap" && ($_POST['host'] == '*.' || $_POST['host'] == '*' || $_POST['host'] == '@.' || $_POST['host'] == '@')) { $host_to_check = $_POST['domainname']; @@ -115,6 +116,9 @@ if ($_POST['save'] || $_POST['force']) { $host_to_check = $_POST['domainname']; } elseif (($pconfig['type'] == "digitalocean") && ($_POST['host'] == '@.' || $_POST['host'] == '@')) { $host_to_check = $_POST['domainname']; + } elseif (($pconfig['type'] == "linode") || ($pconfig['type'] == "linode-v6")) { + $host_to_check = $_POST['host'] == '@' ? $_POST['domainname'] : ( $_POST['host'] . '.' . $_POST['domainname'] ); + $allow_wildcard = true; } else { $host_to_check = $_POST['host']; @@ -130,7 +134,7 @@ if ($_POST['save'] || $_POST['force']) { } if ($pconfig['type'] != "custom" && $pconfig['type'] != "custom-v6") { - if (!is_domain($host_to_check)) { + if (!is_domain($host_to_check, $allow_wildcard)) { $input_errors[] = gettext("The hostname contains invalid characters."); } } @@ -301,8 +305,8 @@ $group->setHelp('Enter the complete fully qualified domain name. Example: myhost 'he.net tunnelbroker: Enter the tunnel ID.%1$s' . 'GleSYS: Enter the record ID.%1$s' . 'DNSimple: Enter only the domain name.%1$s' . - 'Namecheap, Cloudflare, GratisDNS, Hover, ClouDNS, GoDaddy: Enter the hostname and the domain separately, with the domain being the domain or subdomain zone being handled by the provider.%1$s' . - 'Cloudflare and DigitalOcean: Enter @ as the hostname to indicate an empty field.', '
'); + 'Namecheap, Cloudflare, GratisDNS, Hover, ClouDNS, GoDaddy, Linode: Enter the hostname and the domain separately, with the domain being the domain or subdomain zone being handled by the provider.%1$s' . + 'Cloudflare, DigitalOcean, Linode: Enter @ as the hostname to indicate an empty field.', '
'); $section->add($group); @@ -356,7 +360,7 @@ $section->addInput(new Form_Input( 'Username', 'text', $pconfig['username'] -))->setHelp('Username is required for all types except Namecheap, FreeDNS , FreeDNS-v6, DigitalOcean and Custom Entries.%1$s' . +))->setHelp('Username is required for all types except Namecheap, FreeDNS, FreeDNS-v6, DigitalOcean, Linode and Custom Entries.%1$s' . 'Azure: Enter your Azure AD application ID%1$s' . 'DNS Made Easy: Dynamic DNS ID%1$s' . 'DNSimple: User account ID (In the URL after the \'/a/\')%1$s' . @@ -381,6 +385,7 @@ $section->addPassword(new Form_Input( 'Dreamhost: Enter the API Key.%1$s' . 'GoDaddy: Enter the API secret.%1$s' . 'DNSimple: Enter the API token.%1$s' . + 'Linode: Enter the Personal Access Token.%1$s' . 'Cloudflare: Enter the Global API Key.', '
'); $section->addInput(new Form_Input( @@ -589,6 +594,21 @@ events.push(function() { hideInput('zoneid', false); hideInput('ttl', false); break; + case "linode-v6": + case "linode": + hideGroupInput('domainname', false); + hideInput('resultmatch', true); + hideInput('updateurl', true); + hideInput('requestif', true); + hideCheckbox('curl_ipresolve_v4', true); + hideCheckbox('curl_ssl_verifypeer', true); + hideInput('host', false); + hideInput('mx', true); + hideCheckbox('wildcard', true); + hideCheckbox('proxied', true); + hideInput('zoneid', true); + hideInput('ttl', false); + break; default: hideGroupInput('domainname', true); hideInput('resultmatch', true);