Merge branch 'nfsn-wip' of github.com:Frogging101/ddclient into merge_Frogging101
This commit is contained in:
commit
321ca60d04
3 changed files with 276 additions and 4 deletions
|
@ -31,6 +31,7 @@ Dynamic DNS services currently supported include:
|
||||||
Yandex - See https://domain.yandex.com/ for details
|
Yandex - See https://domain.yandex.com/ for details
|
||||||
DNS Made Easy - See https://dnsmadeeasy.com/ for details
|
DNS Made Easy - See https://dnsmadeeasy.com/ for details
|
||||||
DonDominio - See https://www.dondominio.com for details
|
DonDominio - See https://www.dondominio.com for details
|
||||||
|
NearlyFreeSpeech.net - See https://www.nearlyfreespeech.net/services/dns for details
|
||||||
|
|
||||||
DDclient now supports many of cable/dsl broadband routers.
|
DDclient now supports many of cable/dsl broadband routers.
|
||||||
|
|
||||||
|
|
270
ddclient
270
ddclient
|
@ -598,6 +598,18 @@ my %services = (
|
||||||
$variables{'service-common-defaults'},
|
$variables{'service-common-defaults'},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
'nfsn' => {
|
||||||
|
'updateable' => undef,
|
||||||
|
'update' => \&nic_nfsn_update,
|
||||||
|
'examples' => \&nic_nfsn_examples,
|
||||||
|
'variables' => merge(
|
||||||
|
{ 'server' => setv(T_FQDNP, 1, 0, 1, 'api.nearlyfreespeech.net', undef) },
|
||||||
|
{ 'min_interval' => setv(T_FQDNP, 0, 0, 1, 0, interval('5m')) },
|
||||||
|
{ 'ttl' => setv(T_NUMBER, 1, 0, 1, 300, undef) },
|
||||||
|
{ 'zone' => setv(T_FQDN, 1, 0, 1, undef, undef) },
|
||||||
|
$variables{'service-common-defaults'},
|
||||||
|
),
|
||||||
|
},
|
||||||
'sitelutions' => {
|
'sitelutions' => {
|
||||||
'updateable' => undef,
|
'updateable' => undef,
|
||||||
'update' => \&nic_sitelutions_update,
|
'update' => \&nic_sitelutions_update,
|
||||||
|
@ -1383,8 +1395,8 @@ sub init_config {
|
||||||
$proto = $config{$h}{'protocol'};
|
$proto = $config{$h}{'protocol'};
|
||||||
$proto = opt('protocol') if !defined($proto);
|
$proto = opt('protocol') if !defined($proto);
|
||||||
|
|
||||||
load_sha1_support() if ($proto eq "freedns");
|
load_sha1_support($proto) if (grep (/^$proto$/, ("freedns", "nfsn")));
|
||||||
load_json_support() if (grep (/^$proto$/, ("cloudflare","yandex")));
|
load_json_support($proto) if (grep (/^$proto$/, ("cloudflare","yandex","nfsn")));
|
||||||
|
|
||||||
if (!exists($services{$proto})) {
|
if (!exists($services{$proto})) {
|
||||||
warning("skipping host: %s: unrecognized protocol '%s'", $h, $proto);
|
warning("skipping host: %s: unrecognized protocol '%s'", $h, $proto);
|
||||||
|
@ -1969,11 +1981,12 @@ EOM
|
||||||
## load_sha1_support
|
## load_sha1_support
|
||||||
######################################################################
|
######################################################################
|
||||||
sub load_sha1_support {
|
sub load_sha1_support {
|
||||||
|
my $why = shift;
|
||||||
my $sha1_loaded = eval {require Digest::SHA1};
|
my $sha1_loaded = eval {require Digest::SHA1};
|
||||||
my $sha_loaded = eval {require Digest::SHA};
|
my $sha_loaded = eval {require Digest::SHA};
|
||||||
unless ($sha1_loaded || $sha_loaded) {
|
unless ($sha1_loaded || $sha_loaded) {
|
||||||
fatal(<<"EOM");
|
fatal(<<"EOM");
|
||||||
Error loading the Perl module Digest::SHA1 or Digest::SHA needed for freedns update.
|
Error loading the Perl module Digest::SHA1 or Digest::SHA needed for $why update.
|
||||||
On Debian, the package libdigest-sha1-perl or libdigest-sha-perl must be installed.
|
On Debian, the package libdigest-sha1-perl or libdigest-sha-perl must be installed.
|
||||||
EOM
|
EOM
|
||||||
}
|
}
|
||||||
|
@ -1987,10 +2000,11 @@ EOM
|
||||||
## load_json_support
|
## load_json_support
|
||||||
######################################################################
|
######################################################################
|
||||||
sub load_json_support {
|
sub load_json_support {
|
||||||
|
my $why = shift;
|
||||||
my $json_loaded = eval {require JSON::PP};
|
my $json_loaded = eval {require JSON::PP};
|
||||||
unless ($json_loaded) {
|
unless ($json_loaded) {
|
||||||
fatal(<<"EOM");
|
fatal(<<"EOM");
|
||||||
Error loading the Perl module JSON::PP needed for cloudflare update.
|
Error loading the Perl module JSON::PP needed for $why update.
|
||||||
EOM
|
EOM
|
||||||
}
|
}
|
||||||
import JSON::PP (qw/decode_json/);
|
import JSON::PP (qw/decode_json/);
|
||||||
|
@ -2352,6 +2366,35 @@ sub group_hosts_by {
|
||||||
}
|
}
|
||||||
return %groups;
|
return %groups;
|
||||||
}
|
}
|
||||||
|
######################################################################
|
||||||
|
## encode_www_form_urlencoded
|
||||||
|
######################################################################
|
||||||
|
sub encode_www_form_urlencoded {
|
||||||
|
my $formdata = shift;
|
||||||
|
|
||||||
|
my $must_encode = qr'[<>"#%{}|\\^~\[\]`;/?:=&+]';
|
||||||
|
my $encoded;
|
||||||
|
my $i = 0;
|
||||||
|
foreach my $k (keys %$formdata) {
|
||||||
|
my $kenc = $k;
|
||||||
|
my $venc = $formdata->{$k};
|
||||||
|
|
||||||
|
$kenc =~ s/($must_encode)/sprintf('%%%02X', ord($1))/ge;
|
||||||
|
$venc =~ s/($must_encode)/sprintf('%%%02X', ord($1))/ge;
|
||||||
|
|
||||||
|
$kenc =~ s/ /+/g;
|
||||||
|
$venc =~ s/ /+/g;
|
||||||
|
|
||||||
|
$encoded .= $kenc.'='.$venc;
|
||||||
|
if ($i < (keys %$formdata) - 1) {
|
||||||
|
$encoded .= '&';
|
||||||
|
}
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $encoded;
|
||||||
|
}
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
## nic_examples
|
## nic_examples
|
||||||
######################################################################
|
######################################################################
|
||||||
|
@ -3714,6 +3757,225 @@ sub nic_namecheap_update {
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
## nic_nfsn_examples
|
||||||
|
######################################################################
|
||||||
|
sub nic_nfsn_examples {
|
||||||
|
return <<EoEXAMPLE;
|
||||||
|
|
||||||
|
o 'nfsn'
|
||||||
|
|
||||||
|
The 'nfsn' protocol is used for the DNS service offered by www.nearlyfreespeech.net.
|
||||||
|
|
||||||
|
Configuration variables applicable to the 'nfsn' protocol are:
|
||||||
|
protocol=nfsn
|
||||||
|
server=api-server ## defaults to api.nearlyfreespeech.net
|
||||||
|
login=member-login ## NearlyFreeSpeech.net login name
|
||||||
|
password=api-key ## NearlyFreeSpeech.net API key
|
||||||
|
zone=zone ## The DNS zone under which the hostname falls; e.g. example.com
|
||||||
|
hostname ## the hostname to update in the specified zone; e.g. example.com or www.example.com
|
||||||
|
|
||||||
|
Example ${program}.conf file entries:
|
||||||
|
## update two hosts (example.com and www.example.com) in example.com zone
|
||||||
|
protocol=nfsn, \\
|
||||||
|
login=my-nfsn-member-login, \\
|
||||||
|
password=my-nfsn-api-key, \\
|
||||||
|
zone=example.com \\
|
||||||
|
example.com,www.example.com
|
||||||
|
|
||||||
|
## repeat the above for other zones, e.g. example.net:
|
||||||
|
[...]
|
||||||
|
zone=example.net \\
|
||||||
|
subdomain1.example.net,subdomain2.example.net
|
||||||
|
|
||||||
|
EoEXAMPLE
|
||||||
|
}
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
## nic_nfsn_gen_auth_header
|
||||||
|
######################################################################
|
||||||
|
sub nic_nfsn_gen_auth_header {
|
||||||
|
my $h = shift;
|
||||||
|
my $path = shift;
|
||||||
|
my $body = shift || '';
|
||||||
|
|
||||||
|
## API requests must include a custom HTTP header in the
|
||||||
|
## following format:
|
||||||
|
##
|
||||||
|
## X-NFSN-Authentication: login;timestamp;salt;hash
|
||||||
|
##
|
||||||
|
## In this header, login is the member login name of the user
|
||||||
|
## making the API request.
|
||||||
|
my $auth_header = 'X-NFSN-Authentication: ';
|
||||||
|
$auth_header .= $config{$h}{'login'} . ';';
|
||||||
|
|
||||||
|
## timestamp is the standard 32-bit unsigned Unix timestamp
|
||||||
|
## value.
|
||||||
|
my $timestamp = time();
|
||||||
|
$auth_header .= $timestamp . ';';
|
||||||
|
|
||||||
|
## salt is a randomly generated 16 character alphanumeric value
|
||||||
|
## (a-z, A-Z, 0-9).
|
||||||
|
my @chars = ('A'..'Z', 'a'..'z', '0'..'9');
|
||||||
|
my $salt;
|
||||||
|
for (my $i = 0; $i < 16; $i++) {
|
||||||
|
$salt .= $chars[int(rand(@chars))];
|
||||||
|
}
|
||||||
|
$auth_header .= $salt . ';';
|
||||||
|
|
||||||
|
## hash is a SHA1 hash of a string in the following format:
|
||||||
|
## login;timestamp;salt;api-key;request-uri;body-hash
|
||||||
|
my $hash_string = $config{$h}{'login'}.';'.
|
||||||
|
$timestamp . ';' .
|
||||||
|
$salt . ';' .
|
||||||
|
$config{$h}{'password'} . ';';
|
||||||
|
|
||||||
|
## The request-uri value is the path portion of the requested URL
|
||||||
|
## (i.e. excluding the protocol and hostname).
|
||||||
|
$hash_string .= $path . ';';
|
||||||
|
|
||||||
|
## The body-hash is the SHA1 hash of the request body (if any).
|
||||||
|
## If there is no request body, the SHA1 hash of the empty string
|
||||||
|
## must be used.
|
||||||
|
my $body_hash = sha1_hex($body);
|
||||||
|
$hash_string .= $body_hash;
|
||||||
|
|
||||||
|
my $hash = sha1_hex($hash_string);
|
||||||
|
$auth_header .= $hash;
|
||||||
|
|
||||||
|
return $auth_header;
|
||||||
|
}
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
## nic_nfsn_make_request
|
||||||
|
######################################################################
|
||||||
|
sub nic_nfsn_make_request {
|
||||||
|
my $h = shift;
|
||||||
|
my $path = shift;
|
||||||
|
my $method = shift || 'GET';
|
||||||
|
my $body = shift || '';
|
||||||
|
|
||||||
|
my $base_url = "https://$config{$h}{'server'}";
|
||||||
|
my $url = $base_url . $path;
|
||||||
|
my $header = nic_nfsn_gen_auth_header($h, $path, $body);
|
||||||
|
if ($method eq 'POST' && $body ne '') {
|
||||||
|
$header .= "\nContent-Type: application/x-www-form-urlencoded";
|
||||||
|
}
|
||||||
|
|
||||||
|
return geturl(opt('proxy'), $url, '', '', $header, $method, $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
## nic_nfsn_handle_error
|
||||||
|
######################################################################
|
||||||
|
sub nic_nfsn_handle_error {
|
||||||
|
my $resp = shift;
|
||||||
|
my $h = shift;
|
||||||
|
|
||||||
|
$resp =~ s/^.*?\n\n//s; # Strip header
|
||||||
|
my $json = eval {decode_json($resp)};
|
||||||
|
if ($@ || ref($json) ne 'HASH' || not defined $json->{'error'}) {
|
||||||
|
failed("Invalid error response: %s", $resp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
failed($json->{'error'});
|
||||||
|
if (defined $json->{'debug'}) {
|
||||||
|
failed($json->{'debug'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
## nic_nfsn_update
|
||||||
|
##
|
||||||
|
## Written by John Brooks
|
||||||
|
##
|
||||||
|
## Based on API docs: https://members.nearlyfreespeech.net/wiki/API/Introduction
|
||||||
|
## Uses the API endpoints under https://api.nearlyfreespeech.net/dns/$zone/
|
||||||
|
##
|
||||||
|
## NB: There is no "updateRR" API function; to update an existing RR, we use
|
||||||
|
## removeRR to delete the RR, and then addRR to re-add it with the new data.
|
||||||
|
##
|
||||||
|
######################################################################
|
||||||
|
sub nic_nfsn_update {
|
||||||
|
debug("\nnic_nfsn_update -------------------");
|
||||||
|
|
||||||
|
## update each configured host
|
||||||
|
foreach my $h (@_) {
|
||||||
|
my $zone = $config{$h}{'zone'};
|
||||||
|
my $name;
|
||||||
|
|
||||||
|
if ($h eq $zone) {
|
||||||
|
$name = '';
|
||||||
|
} elsif ($h !~ /$zone$/) {
|
||||||
|
$config{$h}{'status'} = 'failed';
|
||||||
|
failed("updating %s: %s is outside zone %s", $h, $h,
|
||||||
|
$zone);
|
||||||
|
next;
|
||||||
|
} else {
|
||||||
|
$name =~ s/(.*)\.${zone}$/$1/;
|
||||||
|
}
|
||||||
|
|
||||||
|
my $ip = delete $config{$h}{'wantip'};
|
||||||
|
info("setting IP address to %s for %s", $ip, $h);
|
||||||
|
verbose("UPDATE", "updating %s", $h);
|
||||||
|
|
||||||
|
my $list_path = "/dns/$zone/listRRs";
|
||||||
|
my $list_body = encode_www_form_urlencoded({name => $name,
|
||||||
|
type => 'A'});
|
||||||
|
my $list_resp = nic_nfsn_make_request($h, $list_path, 'POST',
|
||||||
|
$list_body);
|
||||||
|
if (!header_ok($h, $list_resp)) {
|
||||||
|
$config{$h}{'status'} = 'failed';
|
||||||
|
nic_nfsn_handle_error($list_resp, $h);
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
|
||||||
|
$list_resp =~ s/^.*?\n\n//s; # Strip header
|
||||||
|
my $list = eval{decode_json($list_resp)};
|
||||||
|
if ($@) {
|
||||||
|
$config{$h}{'status'} = 'failed';
|
||||||
|
failed("updating %s: JSON decoding failure", $h);
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
|
||||||
|
my $rr_ttl = $config{$h}{'ttl'};
|
||||||
|
|
||||||
|
if (ref($list) eq 'ARRAY' && defined $list->[0]->{'data'}) {
|
||||||
|
my $rr_data = $list->[0]->{'data'};
|
||||||
|
my $rm_path = "/dns/$zone/removeRR";
|
||||||
|
my $rm_data = {name => $name,
|
||||||
|
type => 'A',
|
||||||
|
data => $rr_data};
|
||||||
|
my $rm_body = encode_www_form_urlencoded($rm_data);
|
||||||
|
my $rm_resp = nic_nfsn_make_request($h, $rm_path,
|
||||||
|
'POST', $rm_body);
|
||||||
|
if (!header_ok($h, $rm_resp)) {
|
||||||
|
$config{$h}{'status'} = 'failed';
|
||||||
|
nic_nfsn_handle_error($rm_resp);
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my $add_path = "/dns/$zone/addRR";
|
||||||
|
my $add_data = {name => $name,
|
||||||
|
type => 'A',
|
||||||
|
data => $ip,
|
||||||
|
ttl => $rr_ttl};
|
||||||
|
my $add_body = encode_www_form_urlencoded($add_data);
|
||||||
|
my $add_resp = nic_nfsn_make_request($h, $add_path, 'POST',
|
||||||
|
$add_body);
|
||||||
|
if (header_ok($h, $add_resp)) {
|
||||||
|
$config{$h}{'ip'} = $ip;
|
||||||
|
$config{$h}{'mtime'} = $now;
|
||||||
|
$config{$h}{'status'} = 'good';
|
||||||
|
success("updating %s: good: IP address set to %s", $h, $ip);
|
||||||
|
} else {
|
||||||
|
$config{$h}{'status'} = 'failed';
|
||||||
|
nic_nfsn_handle_error($add_resp, $h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
|
@ -163,6 +163,15 @@ ssl=yes # use ssl-support. Works with
|
||||||
# password=my-namecheap.com-password \
|
# password=my-namecheap.com-password \
|
||||||
# fully.qualified.host
|
# fully.qualified.host
|
||||||
|
|
||||||
|
##
|
||||||
|
## NearlyFreeSpeech.NET (nearlyfreespeech.net)
|
||||||
|
##
|
||||||
|
# protocol = nfsn, \
|
||||||
|
# login=member-login, \
|
||||||
|
# password=api-key, \
|
||||||
|
# zone=example.com \
|
||||||
|
# example.com,subdomain.example.com
|
||||||
|
|
||||||
##
|
##
|
||||||
##
|
##
|
||||||
## Loopia (loopia.se)
|
## Loopia (loopia.se)
|
||||||
|
|
Loading…
Reference in a new issue