From 7f719dc305d0d8d831c7a977a3ecc4cb5e7d7a3d Mon Sep 17 00:00:00 2001 From: Jimmy Thrasibule Date: Tue, 14 Jul 2020 12:40:47 +0200 Subject: [PATCH] Add support for Gandi LiveDNS Allow update of a DNS record hosted by the Gandi LiveDNS service. Signed-off-by: Jimmy Thrasibule Reviewed-by: Richard Hansen --- ChangeLog.md | 1 + README.md | 1 + ddclient.conf.in | 10 ++++ ddclient.in | 130 ++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 140 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index c813ca3..be8cfa6 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -10,6 +10,7 @@ repository history](https://github.com/ddclient/ddclient/commits/master). * Added support for OVH DynHost. * Added support for ClouDNS. * Added support for dinahosting. + * Added support for Gandi LiveDNS. * Added a build system to make it easier for distributions to package ddclient: diff --git a/README.md b/README.md index 5d39bab..bd0cf62 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Dynamic DNS services currently supported include: OVH - See https://www.ovh.com for details ClouDNS - See https://www.cloudns.net dinahosting - See https://dinahosting.com + Gandi - See https://gandi.net DDclient now supports many of cable/dsl broadband routers. diff --git a/ddclient.conf.in b/ddclient.conf.in index 7c4e6cd..164a260 100644 --- a/ddclient.conf.in +++ b/ddclient.conf.in @@ -196,6 +196,16 @@ ssl=yes # use ssl-support. Works with #password=APIKey \ # This is either your global API key, or an API token. If you are using an API token, it must have the permissions "Zone - DNS - Edit" and "Zone - Zone - Read". The Zone resources must be "Include - All zones". #domain.tld,my.domain.tld +## +## Gandi (gandi.net) +## +## Single host update +# protocol=gandi, \ +# zone=example.com, \ +# password=my-gandi-api-key, \ +# ttl=3h \ +# myhost.example.com + ## ## Google Domains (www.google.com/domains) ## diff --git a/ddclient.in b/ddclient.in index 90cc866..65551e9 100755 --- a/ddclient.in +++ b/ddclient.in @@ -571,6 +571,21 @@ my %services = ( 'server' => setv(T_FQDNP, 1, 0, 'freemyip.com', undef), }, }, + 'gandi' => { + 'updateable' => undef, + 'update' => \&nic_gandi_update, + 'examples' => \&nic_gandi_examples, + 'variables' => { + %{$variables{'service-common-defaults'}}, + 'min-interval' => setv(T_DELAY, 0, 0, 0, interval('5m')), + 'server' => setv(T_FQDNP, 1, 0, 'api.gandi.net', undef), + 'script' => setv(T_STRING, 1, 1, '/v5', undef), + 'ttl' => setv(T_DELAY, 1, 0, interval('3h'), interval('5m')), + 'zone' => setv(T_FQDN, 1, 0, undef, undef), + # Unused variables. + 'login' => setv(T_STRING, 0, 0, 'unused', undef), + } + }, 'googledomains' => { 'updateable' => undef, 'update' => \&nic_googledomains_update, @@ -1394,7 +1409,7 @@ sub init_config { $proto = opt('protocol') if !defined($proto); load_sha1_support($proto) if (grep (/^$proto$/, ("freedns", "nfsn"))); - load_json_support($proto) if (grep (/^$proto$/, ("cloudflare","yandex", "nfsn"))); + load_json_support($proto) if (grep (/^$proto$/, ("cloudflare", "gandi", "yandex", "nfsn"))); if (!exists($services{$proto})) { warning("skipping host: %s: unrecognized protocol '%s'", $h, $proto); @@ -1983,7 +1998,7 @@ sub load_json_support { Error loading the Perl module JSON::PP needed for $why update. EOM } - import JSON::PP (qw/decode_json/); + import JSON::PP (qw/decode_json encode_json/); } ###################################################################### ## geturl @@ -5176,6 +5191,117 @@ sub nic_dinahosting_update { } } +###################################################################### +## nic_gandi_examples +## by Jimmy Thrasibule +###################################################################### +sub nic_gandi_examples { + return <<"EoEXAMPLE"; +o 'gandi' + +The 'gandi' protocol is used by the LiveDNS service offered by gandi.net. + +Description of Gandi's LiveDNS API can be found at: + + https://api.gandi.net/docs/livedns/ + + +Available configuration variables: + * password: The Gandi API key. If you don’t have one yet, you can generate + your production API key from the API Key Page (in the Security section). + Required. + * zone: The DNS zone to be updated. Required. + * ttl: The time-to-live value associated with the updated DNS record. + Optional; defaults to 3h. + +Example ${program}.conf file entries: + ## Single host update. + protocol=gandi, \\ + zone=example.com, \\ + password=my-gandi-api-key, \\ + host.example.com + + ## Multiple host update. + protocol=gandi, \\ + zone=example.com, \\ + password=my-gandi-api-key, \\ + ttl=1h \\ + hosta.example.com,hostb.sub.example.com +EoEXAMPLE +} + +###################################################################### +## nic_gandi_update +###################################################################### +sub nic_gandi_update { + debug("\nnic_gandi_update -------------------"); + + # Update each set configured host. + foreach my $h (@_) { + my $ip = delete $config{$h}{'wantip'}; + (my $hostname = $h) =~ s/\.\Q$config{$h}{zone}\E$//; + + info("%s -- Setting IP address to %s.", $h, $ip); + verbose("UPDATE:", "updating %s", $h); + + my $headers; + $headers = "Content-Type: application/json\n"; + $headers .= "Authorization: Apikey $config{$h}{'password'}\n"; + + my $data = encode_json({ + defined($config{$h}{'ttl'}) ? (rrset_ttl => $config{$h}{'ttl'}) : (), + rrset_values => [$ip], + }); + + my $rrset_type = is_ipv6($ip) ? "AAAA" : "A"; + my $url; + $url = "https://$config{$h}{'server'}$config{$h}{'script'}"; + $url .= "/livedns/domains/$config{$h}{'zone'}/records/$hostname/$rrset_type"; + + my $reply = geturl({ + proxy => opt('proxy'), + url => $url, + headers => $headers, + method => 'PUT', + data => $data, + }); + unless ($reply) { + failed("%s -- Could not connect to %s.", $h, $config{$h}{'server'}); + next; + } + my $ok = header_ok($h, $reply); + + $reply =~ s/^.*?\n\n//s; + my $response = eval { decode_json($reply) }; + if (!defined($response)) { + $config{$h}{'status'} = "bad"; + + failed("%s -- Unexpected service response.", $h); + next; + } + + if ($ok) { + $config{$h}{'ip'} = $ip; + $config{$h}{'mtime'} = $now; + $config{$h}{'status'} = "good"; + + success("%s -- Updated successfully to %s.", $h, $ip); + } else { + $config{$h}{'status'} = "bad"; + + if (defined($response->{status}) && $response->{status} eq "error") { + my @errors; + for my $err (@{$response->{errors}}) { + push(@errors, $err->{description}); + } + failed("%s -- %s.", $h, join(", ", @errors)); + } else { + failed("%s -- Unexpected service response.", $h); + } + } + } +} + # Execute main() if this file is run as a script or run via PAR (https://metacpan.org/pod/PAR), # otherwise do nothing. This "modulino" pattern makes it possible to import this file as a module # and test its functions directly; there's no need for test-only command-line arguments or stdout