From 9913e0ec2997568158d97830f2c373b7ad658bd5 Mon Sep 17 00:00:00 2001 From: jortkoopmans Date: Tue, 17 Oct 2023 23:01:34 +0200 Subject: [PATCH] DNSExit update for ipv6. - Support wantipv4 and wantipv6 configs automatically - Remove manual record type setting - Add support for hosts on your zone (subdomain) - Update config examples - Code/logic improvements --- ddclient.in | 101 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 60 insertions(+), 41 deletions(-) diff --git a/ddclient.in b/ddclient.in index f852c5e..e30524f 100755 --- a/ddclient.in +++ b/ddclient.in @@ -535,9 +535,9 @@ my %variables = ( 'dnsexit2-common-defaults' => { 'ssl' => setv(T_BOOL, 0, 0, 1, undef), 'server' => setv(T_FQDNP, 1, 0, 'api.dnsexit.com', undef), - 'path' => setv(T_STRING, 0, 1, '/dns/', undef), - 'record-type' => setv(T_STRING, 1, 0, 'A', undef), + 'path' => setv(T_STRING, 0, 0, '/dns/', undef), 'ttl' => setv(T_NUMBER, 1, 0, 5, 0), + 'zone' => setv(T_STRING, 0, 0, undef, undef) }, 'regfishde-common-defaults' => { 'server' => setv(T_FQDNP, 1, 0, 'dyndns.regfish.de', undef), @@ -4050,24 +4050,29 @@ sub nic_dnsexit2_examples { return <<"EoEXAMPLE"; o 'dnsexit2' -The 'dnsexit2' protocol is the new API protocol used by the dynamic hostname services -of the 'DNSExit' dns services. This is currently used by the free -dynamic DNS service offered by www.dnsexit.com. +The 'dnsexit2' protocol is the updated protocol for the (free) dynamic hostname services +of 'DNSExit' (www.dnsexit.com). Their API is accepting JSON payload. Configuration variables applicable to the 'dnsexit2' protocol are: protocol=dnsexit2 ## password=YourAPIKey ## API Key of your account. server=api.dnsexit.com ## defaults to api.dnsexit.com. path=/dns/ ## defaults to /dns/. - record-type=A ## defaults to A record. ttl=5 ## defaults to 5 minutes. + zone='' ## defaults to empty, which assumes the zone is equal to the fully.qualified.host (is root of your DNSExit domain). fully.qualified.host ## the host registered with the service. Example ${program}.conf file entries: ## single host update protocol=dnsexit2 password=YourAPIKey - fully.qualified.host + yourown.publicvm.com + + ## two hosts (which must be) on the same zone + protocol=dnsexit2 + password=YourAPIKey + zone=yourown.publicvm.com + host1.yourown.publicvm.com,host2.yourown.publicvm.com EoEXAMPLE } @@ -4081,7 +4086,7 @@ EoEXAMPLE sub nic_dnsexit2_update { debug("\nnic_dnsexit2_update -------------------"); - ## Update each configured host + ## Update each configured host (hosts cannot be grouped on this API) foreach my $h (@_) { # All the known status my %status = ( @@ -4094,25 +4099,45 @@ sub nic_dnsexit2_update { '6' => [ 'error', 'System Error. Our system problem. May not be your problem. Contact our support if you got such error.' ], '7' => [ 'error', 'Error getting post data. Our server has problem to receive your JSON posting.' ], ); - my $ip = delete $config{$h}{'wantip'}; - info("Going to update IP address to %s for %s.", $ip, $h); + my $ipv4 = delete $config{$h}{'wantipv4'}; + my $ipv6 = delete $config{$h}{'wantipv6'}; + + # Updates for ipv4 and ipv6 need to be combined in a single API call, create Hash of Arrays for tracking + my %total_payload; + + foreach my $ip ($ipv4, $ipv6){ + next if (!$ip); + my $ipv = ($ip eq ($ipv6 // '')) ? '6' : '4'; + my $type = ($ip eq ($ipv6 // '')) ? 'AAAA' : 'A'; + + info("Going to update IPv$ipv address to %s for %s.", $ip, $h); + $config{$h}{'status-ipv$ipv'} = 'failed'; + + # One key per ipv (4 or 6) + my %payload = (name => $h, type => $type, content => $ip, ttl => $config{$h}{'ttl'}); + @total_payload{$ipv} = \%payload; + }; # Set the URL of the API endpoint my $url = "https://$config{$h}{'server'}$config{$h}{'path'}"; - # Set JSON payload - my $data = encode_json({ - apikey => $config{$h}{'password'}, - domain => $h, - update => { - type => $config{$h}{'record-type'}, - name => $h, - content => $ip, - ttl => $config{$h}{'ttl'}}, - }); - # Set additional headers - my $header = "Content-Type: application/json\n"; - $header .= "Accept: application/json"; + my $header = "Content-Type: application/json\nAccept: application/json"; + + # Set the zone if empty + if ( not defined $config{$h}{'zone'}){ + debug("Zone not defined, setting to default hostname: %s", $h); + $config{$h}{'zone'} = $h + } else { + debug("Zone is: %s", $config{$h}{'zone'}); + } + + # Build total JSON payload + my @payload_values = values %total_payload; + my $data = encode_json({ + apikey => $config{$h}{'password'}, + domain => $config{$h}{'zone'}, + update => \@payload_values + }); # Make the call my $reply = geturl( @@ -4120,32 +4145,27 @@ sub nic_dnsexit2_update { url => $url, headers => $header, method => 'POST', - data => $data, + data => $data ); # No reply, declare as failed - if (!defined($reply) || !$reply) { + unless ($reply && header_ok($h, $reply)){ failed("updating %s: Could not connect to %s%s.", $h, $config{$h}{'server'}, $config{$h}{'path'}); - $config{$h}{'status'} = 'failed'; last; }; # Reply found debug("%s", $reply); - # $ok is mandatory? - my $ok = header_ok($h, $reply); - # Extract the HTTP response code (my $http_status) = ($reply =~ m%^s*HTTP/.*\s+(\d+)%i); debug("HTTP response code: %s", $http_status); # If not 200, bail - if ( $http_status != "200"){ - failed("Failed to update Host\n%s to IP:%s", $h, $ip); + if ( $http_status ne '200' ){ + failed("Failed to update Host\n%s", $h); failed("HTTP response code\n%s", $http_status); failed("Full reply\n%s", $reply) unless opt('verbose'); - $config{$h}{'status'} = 'failed'; - last; + next; } # Strip HTTP response headers @@ -4182,31 +4202,30 @@ sub nic_dnsexit2_update { # Handle statuses if ($status eq 'good') { - $config{$h}{'ip'} = $ip; $config{$h}{'mtime'} = $now; - $config{$h}{'status'} = 'good'; - success("%s", $message); - success("Updated %s successfully to IP address %s at time %s", $h, $ip, prettytime($config{$h}{'mtime'})); + my $tracked_ipv; + foreach $tracked_ipv ( keys %total_payload ){ + $config{$h}{"ipv$tracked_ipv"} = $total_payload{$tracked_ipv}{content}; + $config{$h}{"status-ipv$tracked_ipv"} = 'good'; + success("%s", $message); + success("Updated %s successfully to IPv$tracked_ipv address %s at time %s", $h, $total_payload{$tracked_ipv}{content}, prettytime($config{$h}{'mtime'})); + } } elsif ($status eq 'warning') { warning("%s", $message); warning("Server response: %s", $srv_message); } elsif ($status =~ m'^(badauth|error)$') { failed("%s", $message); failed("Server response: %s", $srv_message); - $config{$h}{'status'} = 'failed'; } else { failed("This should not be possible"); - $config{$h}{'status'} = 'failed'; } } else { failed("Status code %s is unknown!", $response->{'code'}); - $config{$h}{'status'} = 'failed'; } } else { failed("Did not receive expected \"code\" and \"message\" keys in server response."); failed("Response:"); failed("%s", $response); - $config{$h}{'status'} = 'failed'; } } }