diff --git a/ChangeLog.md b/ChangeLog.md
index 9e8ed3a..3d603c7 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -15,6 +15,9 @@ repository history](https://github.com/ddclient/ddclient/commits/master).
* The default web service for `--webv4` and `--webv6` has changed from Google
Domains (which has shut down) to ipify.
[5b104ad1](https://github.com/ddclient/ddclient/commit/5b104ad116c023c3760129cab6e141f04f72b406)
+ * Invalid command-line options or values are now fatal errors (instead of
+ discarded with a warning).
+ [#TODO](https://github.com/ddclient/ddclient/pull/TODO)
* All log messages are now written to STDERR, not a mix of STDOUT and STDERR.
[#676](https://github.com/ddclient/ddclient/pull/676)
* For `--protocol=freedns` and `--protocol=nfsn`, the core module
@@ -95,6 +98,8 @@ repository history](https://github.com/ddclient/ddclient/commits/master).
### Bug fixes
+ * Fixed numerous bugs in command-line option and configuration file
+ processing. [#TODO](https://github.com/ddclient/ddclient/pull/TODO)
* `noip`: Fixed failure to honor IP discovery settings in some circumstances.
[#591](https://github.com/ddclient/ddclient/pull/591)
* Fixed `--usev6` with providers that have not yet been updated to use the new
diff --git a/ddclient.in b/ddclient.in
index 4b4e1b7..64ed848 100755
--- a/ddclient.in
+++ b/ddclient.in
@@ -177,7 +177,6 @@ sub T_LOGIN { 'login' }
sub T_PASSWD { 'password' }
sub T_BOOL { 'boolean value' }
sub T_FQDN { 'fully qualified host name' }
-sub T_OFQDN { 'optional fully qualified host name' }
sub T_FILE { 'file name' }
sub T_FQDNP { 'fully qualified host name and optional port number' }
sub T_PROTO { 'protocol' }
@@ -614,7 +613,6 @@ our %variables = (
'debug' => setv(T_BOOL, 0, 0, 0, undef),
'verbose' => setv(T_BOOL, 0, 0, 0, undef),
'quiet' => setv(T_BOOL, 0, 0, 0, undef),
- 'help' => setv(T_BOOL, 0, 0, 0, undef),
'test' => setv(T_BOOL, 0, 0, 0, undef),
'postscript' => setv(T_POSTS, 0, 0, undef, undef),
@@ -705,7 +703,7 @@ our %variables = (
},
'dyndns-common-defaults' => {
'backupmx' => setv(T_BOOL, 0, 1, 0, undef),
- 'mx' => setv(T_OFQDN, 0, 1, undef, undef),
+ 'mx' => setv(T_FQDN, 0, 1, undef, undef),
'wildcard' => setv(T_BOOL, 0, 1, 0, undef),
},
);
@@ -735,7 +733,7 @@ our %protocols = (
'backupmx' => setv(T_BOOL, 0, 1, 0, undef),
'login' => setv(T_LOGIN, 0, 0, 'token', undef),
'min-interval' => setv(T_DELAY, 0, 0, interval('5m'), 0),
- 'mx' => setv(T_OFQDN, 0, 1, undef, undef),
+ 'mx' => setv(T_FQDN, 0, 1, undef, undef),
'server' => setv(T_FQDNP, 0, 0, 'api.cloudflare.com/client/v4', undef),
'static' => setv(T_BOOL, 0, 1, 0, undef),
'ttl' => setv(T_NUMBER, 0, 0, 1, undef),
@@ -862,7 +860,7 @@ our %protocols = (
# From : "You need to wait at least 10
# minutes between updates."
'min-interval' => setv(T_DELAY, 0, 0, interval('10m'), 0),
- 'mx' => setv(T_OFQDN, 0, 1, undef, undef),
+ 'mx' => setv(T_FQDN, 0, 1, undef, undef),
'server' => setv(T_FQDNP, 0, 0, 'api.cp.easydns.com', undef),
'script' => setv(T_STRING, 0, 1, '/dyn/generic.php', undef),
'wildcard' => setv(T_BOOL, 0, 1, 0, undef),
@@ -1018,7 +1016,7 @@ our %protocols = (
'password' => undef,
'apikey' => setv(T_PASSWD, 1, 0, undef, undef),
'secretapikey' => setv(T_PASSWD, 1, 0, undef, undef),
- 'root-domain' => setv(T_OFQDN, 0, 0, undef, undef),
+ 'root-domain' => setv(T_FQDN, 0, 0, undef, undef),
'on-root-domain' => setv(T_BOOL, 0, 0, 0, undef),
},
},
@@ -1047,7 +1045,7 @@ our %protocols = (
%{$variables{'protocol-common-defaults'}},
'min-interval' => setv(T_DELAY, 0, 0, interval('10m'), 0),
'server' => setv(T_FQDNP, 0, 0, 'dynamic.zoneedit.com', undef),
- 'zone' => setv(T_OFQDN, 0, 0, undef, undef),
+ 'zone' => setv(T_FQDN, 0, 0, undef, undef),
},
},
'keysystems' => {
@@ -1214,9 +1212,9 @@ my @opt = (
"",
["login", "=s", "--login= : log in to the dynamic DNS service as "],
["password", "=s", "--password= : log in to the dynamic DNS service with password "],
- ["host", "=s", "--host= : update DNS information for "],
+ ["host", "=s", "--host=[,,...]\n : only update the given hosts. The hosts must already be defined in the config file (see '--file') unless '--options' is also specified"],
"",
- ["options", "=s", "--options==[,=,...]\n : optional per-service arguments (see below)"],
+ ["options", "=s", "--options==[,=,...]\n : override settings from the config file (see '--file') with the given values. Applies to all hosts"],
"",
["ssl", "!", '--{no}ssl : use encryption (TLS) when the scheme (either "http://" or "https://") is missing from a URL'],
["ssl_ca_dir", "=s", "--ssl_ca_dir= : look in for certificates of trusted certificate authorities (default: auto-detect)"],
@@ -1246,19 +1244,18 @@ my @opt = (
"",
nic_examples(),
);
+$opt{'help'} = sub {
+ print(usage(@opt), "\n");
+ $opt{'version'}('', '');
+};
sub main {
- my $opt_usage = process_args(@opt);
+ process_args(@opt);
$saved_recap = '';
%saved_opt = %opt;
$result = 'OK';
- if (opt('help')) {
- printf "%s\n", $opt_usage;
- $opt{'version'}('', '');
- }
-
## read config file because 'daemon' mode may be defined there.
- read_config($opt{'file'} // default('file'), \%config, \%globals);
+ read_config(opt('file'), \%config, \%globals);
init_config();
test_possible_ip() if opt('query');
@@ -1291,25 +1288,10 @@ sub main {
$now = time;
$result = 'OK';
%opt = %saved_opt;
- if (opt('help')) {
- *STDERR = *STDOUT;
- printf("Help found");
- }
-
- read_config($opt{'file'} // default('file'), \%config, \%globals);
+ read_config(opt('file'), \%config, \%globals);
init_config();
read_recap(opt('cache'), \%recap);
print_info() if opt('debug') && opt('verbose');
-
- fatal("invalid argument '--use=%s'; possible values are:\n%s",
- $opt{'use'}, join("\n", ip_strategies_usage()))
- if defined(opt('use')) && !$ip_strategies{lc(opt('use'))};
- if (defined($opt{'usev6'})) {
- fatal("invalid argument '--usev6=%s'; possible values are:\n%s",
- $opt{'usev6'}, join("\n", ipv6_strategies_usage()))
- unless exists $ipv6_strategies{lc opt('usev6')};
- }
-
$daemon = opt('daemon');
update_nics();
@@ -1355,12 +1337,12 @@ sub main {
sub runpostscript {
my ($ip) = @_;
- if (defined $globals{postscript}) {
- my @postscript = split(/\s+/, $globals{postscript});
+ if (defined(my $ps = opt('postscript'))) {
+ my @postscript = split(/\s+/, $ps);
if (-x $postscript[0]) {
- system("$globals{postscript} $ip &");
+ system("$ps $ip &");
} else {
- warning("Can not execute post script: %s", $globals{postscript});
+ warning("Can not execute post script: %s", $ps);
}
}
}
@@ -1380,15 +1362,12 @@ sub update_nics {
for my $h (sort keys %config) {
local $_l = pushlogctx($h);
- next if $config{$h}{'protocol'} ne lc($p);
+ next if opt('protocol', $h) ne $p;
$examined{$h} = 1;
# we only do this once per 'use' and argument combination
- my $use = opt('use', $h) // 'disabled';
- my $usev4 = opt('usev4', $h) // 'disabled';
- my $usev6 = opt('usev6', $h) // 'disabled';
- $use = 'disabled' if ($use eq 'no'); # backward compatibility
- $usev6 = 'disabled' if ($usev6 eq 'no'); # backward compatibility
- $use = 'disabled' if ($usev4 ne 'disabled') || ($usev6 ne 'disabled');
+ my $use = opt('use', $h);
+ my $usev4 = opt('usev4', $h);
+ my $usev6 = opt('usev6', $h);
my $arg_ip = opt('ip', $h) // '';
my $arg_ipv4 = opt('ipv4', $h) // '';
my $arg_ipv6 = opt('ipv6', $h) // '';
@@ -1500,7 +1479,7 @@ sub update_nics {
local $_l = pushlogctx($h);
if (!exists $examined{$h}) {
failed("not updated because protocol is not supported: " .
- $config{$h}{'protocol'} // '');
+ opt('protocol', $h) // '');
}
}
write_recap(opt('cache'));
@@ -1540,15 +1519,20 @@ sub write_recap {
my ($file) = @_;
for my $h (keys %config) {
+ # nic_updateable (called from update_nics for each host) sets `$config{$h}{update}`
+ # according to whether an update was attempted.
+ #
+ # TODO: Why are different variables saved to the recap depending on whether an update was
+ # attempted or not? Shouldn't the same variables be saved every time?
if (!exists $recap{$h} || $config{$h}{'update'}) {
- my $vars = $protocols{$config{$h}{protocol}}{variables};
+ my $vars = $protocols{opt('protocol', $h)}{variables};
for my $v (keys(%$vars)) {
- next if !$vars->{$v}{recap} || !defined($config{$h}{$v});
- $recap{$h}{$v} = $config{$h}{$v};
+ next if !$vars->{$v}{recap} || !defined(opt($v, $h));
+ $recap{$h}{$v} = opt($v, $h);
}
} else {
for my $v (qw(atime wtime status status-ipv4 status-ipv6)) {
- $recap{$h}{$v} = $config{$h}{$v};
+ $recap{$h}{$v} = opt($v, $h);
}
}
}
@@ -1602,6 +1586,8 @@ sub read_recap {
for my $h (keys(%recap)) {
next if !exists($config->{$h});
+ # TODO: Why is this limited to this set of variables? Why not copy every recap var
+ # defined for the host's protocol?
for (qw(atime mtime wtime ip ipv4 ipv6 status status-ipv4 status-ipv6)) {
# TODO: Isn't $config equal to \%recap here? If so, this is a no-op. What was the
# original intention behind this? To copy %recap values into %config? If so, is
@@ -1616,6 +1602,9 @@ sub read_recap {
######################################################################
## parse_assignments(string) return (rest, %variables)
## parse_assignment(string) return (name, value, rest)
+#
+# Parsing stops upon encountering non-assignment text (e.g., hostname after the assignments) or an
+# unquoted/unescaped newline.
######################################################################
sub parse_assignments {
my ($rest) = @_;
@@ -1623,7 +1612,7 @@ sub parse_assignments {
while (1) {
(my $name, my $value, $rest) = parse_assignment($rest);
- $rest =~ s/^[,\s]+//;
+ $rest =~ s/^(?:[^\S\n]|,)+//; # Remove leading commas and non-newline whitespace.
return ($rest, %variables) if !defined($name);
if ($name eq 'fw-banlocal' || $name eq 'if-skip') {
warning("'$name' is deprecated and does nothing");
@@ -1637,7 +1626,9 @@ sub parse_assignment {
my ($name, $value);
my ($escape, $quote) = (0, '');
- if ($rest =~ /^[,\s]*([a-z][0-9a-z_-]*)=(.*)/i) {
+ # Ignore leading commas and non-newline whitespace. (An unquoted/unescaped newline terminates
+ # the assignment search.)
+ if ($rest =~ qr/^(?:[^\S\n]|,)*([a-z][0-9a-z_-]*)=(.*)/is) {
($name, $rest, $value) = ($1, $2, '');
while (length(my $c = substr($rest, 0, 1))) {
@@ -1658,6 +1649,15 @@ sub parse_assignment {
}
$rest = substr($rest,1);
}
+ if ($name =~ qr/^(.*)_env$/) {
+ $name = $1;
+ debug("Loading value for $name from environment variable $value");
+ if (!exists($ENV{$value})) {
+ warning("Environment variable '$value' not set for keyword '$name' (ignored)");
+ return parse_assignment($rest);
+ }
+ $value = $ENV{$value};
+ }
}
warning("assignment to '%s' ended with the escape character (\\)", $name) if $escape;
warning("assignment to '%s' ended with an unterminated quote (%s)", $name, $quote) if $quote;
@@ -1760,6 +1760,8 @@ sub _read_config {
}
## remove comments
+ # TODO: This makes it impossible to include '#' in keys or values except as permitted by
+ # the special password parsing above.
s/#.*//;
## Handle continuation lines
@@ -1778,6 +1780,8 @@ sub _read_config {
s/^\s+//; # remove leading white space
s/\s+$//; # remove trailing white space
+ # TODO: This makes it impossible to include multiple consecutive spaces, tabs, etc. in keys
+ # or values.
s/\s+/ /g; # canonify
next if /^$/;
@@ -1788,34 +1792,20 @@ sub _read_config {
## verify that keywords are valid...and check the value
for my $k (keys %locals) {
- # Handle '_env' keyword suffix
- if ($k =~ /(.*)_env$/) {
- debug("Loading value for $1 from environment variable $locals{$k}.");
- if (!exists($ENV{$locals{$k}})) {
- warning("Environment variable '$locals{$k}' not set for keyword '$k' (ignored)");
- delete $locals{$k};
- next;
- }
- # Set the value to the value of the environment variable
- $locals{$1} = $ENV{$locals{$k}};
- # Remove the '_env' suffix from the key
- $k = $1;
- }
-
$locals{$k} = $passwords{$k} if defined $passwords{$k};
if (!exists $variables{'merged'}{$k}) {
warning("unrecognized keyword '%s' (ignored)", $k);
delete $locals{$k};
next;
}
+ # TODO: This might grab an arbitrary protocol-specific variable definition, which could
+ # cause surprising behavior.
my $def = $variables{'merged'}{$k};
- my $value = check_value($locals{$k}, $def);
- if (!defined($value)) {
- warning("Invalid Value for keyword '%s' = '%s'", $k, $locals{$k});
+ if (!eval { $locals{$k} = check_value($locals{$k}, $def); 1; }) {
+ warning("invalid variable value '$k=$locals{$k}': $@");
delete $locals{$k};
next;
}
- $locals{$k} = $value;
}
%passwords = ();
if (exists($locals{'host'})) {
@@ -1866,53 +1856,27 @@ sub _read_config {
######################################################################
sub init_config {
%opt = %saved_opt;
+ # TODO: This might grab an arbitrary protocol-specific variable definition, which could cause
+ # surprising behavior.
+ for my $var (keys(%{$variables{'merged'}})) {
+ # TODO: Also validate $opt{'options'}.
+ next if !defined($opt{$var}) || ref($opt{$var});
+ if (!eval { $opt{$var} = check_value($opt{$var}, $variables{'merged'}{$var}); 1; }) {
+ fatal("invalid argument '--$var=$opt{$var}': $@");
+ }
+ }
##
$opt{'quiet'} = 0 if opt('verbose');
- ## infer the IP strategy if possible
- if (!$opt{'use'}) {
- $opt{'use'} = 'web' if ($opt{'web'});
- $opt{'use'} = 'if' if ($opt{'if'});
- $opt{'use'} = 'ip' if ($opt{'ip'});
- }
- ## infer the IPv4 strategy if possible
- if (!$opt{'usev4'}) {
- $opt{'usev4'} = 'webv4' if ($opt{'webv4'});
- $opt{'usev4'} = 'ifv4' if ($opt{'ifv4'});
- $opt{'usev4'} = 'ipv4' if ($opt{'ipv4'});
- }
- ## infer the IPv6 strategy if possible
- if (!$opt{'usev6'}) {
- $opt{'usev6'} = 'webv6' if ($opt{'webv6'});
- $opt{'usev6'} = 'ifv6' if ($opt{'ifv6'});
- $opt{'usev6'} = 'ipv6' if ($opt{'ipv6'});
- }
-
- ## sanity check
- $opt{'max-interval'} = min(interval(opt('max-interval')), interval(default('max-interval')));
- $opt{'min-interval'} = max(interval(opt('min-interval')), interval(default('min-interval')));
- $opt{'min-error-interval'} = max(interval(opt('min-error-interval')), interval(default('min-error-interval')));
-
- $opt{'timeout'} = 0 if opt('timeout') < 0;
-
- ## parse an interval expression (such as '5m') into number of seconds
- $opt{'daemon'} = interval(opt('daemon')) if defined($opt{'daemon'});
- ## make sure the interval isn't too short
- $opt{'daemon'} = minimum('daemon') if opt('daemon') && opt('daemon') < minimum('daemon');
-
## define or modify host options specified on the command-line
if (defined($opt{'options'})) {
- ## collect cmdline configuration options.
- my %options = ();
- for my $opt (split_by_comma($opt{'options'})) {
- my ($name, $var) = split /\s*=\s*/, $opt;
- if ($name eq 'fw-banlocal' || $name eq 'if-skip') {
- warning("'$name' is deprecated and does nothing");
- next;
- }
- $options{$name} = $var;
- }
+ # TODO: Perhaps the --options argument should be processed like the contents of the config
+ # file: each line (after removing any comments or continuations) either specifies global
+ # values or host-specific settings. For now, non-value newlines and end-of-line host
+ # declarations are rejected.
+ my ($rest, %options) = parse_assignments($opt{'options'});
+ fatal("unexpected content in '--options' argument: $rest") if $rest ne '';
## determine hosts specified with --host
my @hosts = ();
if (exists $opt{'host'}) {
@@ -1930,18 +1894,32 @@ sub init_config {
## merge options into host definitions or globals
if (@hosts) {
for my $h (@hosts) {
- $config{$h} = {%{$config{$h} // {}}, %options, 'host' => $h};
+ $config{$h} //= {'host' => $h};
+ my $proto = $options{'protocol'} // opt('protocol', $h);
+ my $protodef = $protocols{$proto} or fatal("host $h: invalid protocol: $proto");
+ for my $var (keys(%options)) {
+ my $def = $protodef->{variables}{$var}
+ or fatal("host $h: unknown option '--options=$var=$options{$var}'");
+ eval { $config{$h}{$var} = check_value($options{$var}, $def); 1; }
+ or fatal("host $h: invalid option value '--options=$var=$options{$var}': $@");
+ }
}
$opt{'host'} = join(',', @hosts);
} else {
- %globals = (%globals, %options);
+ for my $var (keys(%options)) {
+ # TODO: This might grab an arbitrary protocol-specific variable definition, which
+ # could cause surprising behavior.
+ my $def = $variables{'merged'}{$var}
+ or fatal("unknown option '--options=$var=$options{$var}'");
+ # TODO: Why not merge the values into %opt?
+ eval { $globals{$var} = check_value($options{$var}, $def); 1; }
+ or fatal("invalid option value '--options=$var=$options{$var}': $@");
+ }
}
}
## override global options with those on the command-line.
for my $o (keys %opt) {
- # TODO: Isn't $opt{$o} guaranteed to be defined? Otherwise $o wouldn't appear in the keys
- # of %opt, right?
# TODO: Why is this limited to $variables{'global-defaults'}? Why not
# $variables{'merged'}?
if (defined $opt{$o} && exists $variables{'global-defaults'}{$o}) {
@@ -1949,10 +1927,13 @@ sub init_config {
# %opt doesn't have a value, so this shouldn't be necessary.
$globals{$o} = $opt{$o};
}
+ # TODO: Why aren't host configs updated with command-line values (except for $opt{options}
+ # handled above)? Shouldn't command-line values always override config file values (even
+ # if they are not associated with a host via `--host=` or `--options=host=`)?
}
## sanity check
- if (defined $opt{'host'} && defined $opt{'retry'}) {
+ if (defined(opt('host')) && opt('retry')) {
fatal("options --retry and --host (or --option host=..) are mutually exclusive");
}
fatal("options --retry and --daemon cannot be used together") if (opt('retry') && opt('daemon'));
@@ -1962,7 +1943,9 @@ sub init_config {
if (opt('host')) {
@hosts = split_by_comma($opt{'host'});
}
- # TODO: This function is called before the recap file is read. How is this supposed to work?
+ # TODO: The first two times init_config() is called the cache file has not been read yet, so
+ # this will not filter out any hosts and thus updates will not be limited to non-good hosts as
+ # intended.
if (opt('retry')) {
@hosts = grep(($recap{$_}{'status'} // '') ne 'good', keys(%recap));
}
@@ -1972,83 +1955,28 @@ sub init_config {
map { $hosts{$_} = undef } @hosts;
map { delete $config{$_} unless exists $hosts{$_} } keys %config;
- ## sanity check..
- ## make sure config entries have all defaults and they meet minimums
- ## first the globals...
- for my $k (keys %globals) {
- # Make sure any _env suffixed variables look at their original entry
- $k = $1 if $k =~ /^(.*)_env$/;
+ # TODO: Why aren't the hosts specified by --host added to %config except when --options is also
+ # given?
- # TODO: This might grab an arbitrary protocol-specific variable, which could cause
- # surprising behavior.
- my $def = $variables{'merged'}{$k};
- if (!$def) {
- warning("ignoring unknown setting '$k=$globals{$k}'");
- delete($globals{$k});
- next;
- }
- # TODO: Isn't $globals{$k} guaranteed to be defined here? Otherwise $k wouldn't appear in
- # %globals.
- my $ovalue = $globals{$k} // $def->{'default'};
- # TODO: Didn't _read_config already check the value? Or is the purpose of this to check
- # the value of command-line options ($opt{$k}) which were merged into %globals above?
- my $value = check_value($ovalue, $def);
- if ($def->{'required'} && !defined $value) {
- # TODO: What's the point of this? The opt() function will fall back to the default
- # value if $globals{$k} is undefined.
- $value = default($k);
- warning("'%s=%s' is an invalid %s. (using default of %s)", $k, $ovalue, $def->{'type'}, $value);
- }
- $globals{$k} = $value;
- }
-
- ## now the host definitions...
- HOST:
for my $h (keys %config) {
- my $proto = opt('protocol', $h);
- load_sha1_support($proto) if (grep($_ eq $proto, ("freedns", "nfsn")));
- load_json_support($proto) if (grep($_ eq $proto, ("1984", "cloudflare", "digitalocean", "directnic", "gandi", "godaddy", "hetzner", "yandex", "nfsn", "njalla", "porkbun", "dnsexit2")));
-
- if (!exists($protocols{$proto})) {
- warning("skipping host: %s: unrecognized protocol '%s'", $h, $proto);
- delete $config{$h};
- next;
- }
-
- my $svars = $protocols{$proto}{'variables'};
- my $conf = {'host' => $h, 'protocol' => $proto};
-
- for my $k (keys %$svars) {
- # Make sure any _env suffixed variables look at their original entry
- $k = $1 if $k =~ /^(.*)_env$/;
-
- my $def = $svars->{$k};
- my $ovalue = $config{$h}{$k} // $def->{'default'};
- my $value = check_value($ovalue, $def);
- if ($def->{'required'} && !defined $value) {
- $ovalue //= '(not set)';
- warning("skipping host $h: invalid $def->{type} variable value '$k=$ovalue'");
- delete $config{$h};
- next HOST;
- }
- $conf->{$k} = $value;
- }
- $config{$h} = $conf;
+ $config{$h}{use} = 'disabled'
+ if opt('usev4', $h) ne 'disabled' || opt('usev6', $h) ne 'disabled';
}
+ my @protos = map(opt('protocol', $_), keys(%config));
+ my @needs_sha1 = grep({ my $p = $_; grep($_ eq $p, @protos); } qw(freedns nfsn));
+ load_sha1_support(join(', ', @needs_sha1)) if @needs_sha1;
+ my @needs_json = grep({ my $p = $_; grep($_ eq $p, @protos); }
+ qw(1984 cloudflare digitalocean directnic dnsexit2 gandi godaddy hetzner
+ nfsn njalla porkbun yandex));
+ load_json_support(join(', ', @needs_json)) if @needs_json;
}
-######################################################################
-## process_args -
-######################################################################
-sub process_args {
- my @spec = ();
+sub usage {
my $usage = "";
for (@_) {
if (ref $_) {
my ($key, $specifier, $arg_usage) = @$_;
my $value = default($key);
- push @spec, $key . $specifier;
- $opt{$key} //= undef;
next unless $arg_usage;
$usage .= " $arg_usage";
if (defined($value) && $value ne '') {
@@ -2067,12 +1995,24 @@ sub process_args {
}
$usage .= "\n";
}
- if (!GetOptions(\%opt, @spec)) {
- $opt{"help"} = 1;
- }
return $usage;
}
+######################################################################
+## process_args -
+######################################################################
+sub process_args {
+ my @spec = ();
+ for (@_) {
+ next if !ref($_);
+ my ($key, $specifier) = @$_;
+ push @spec, $key . $specifier;
+ }
+ if (!GetOptions(\%opt, @spec)) {
+ $opt{'help'}('', '');
+ }
+}
+
######################################################################
## test_possible_ip - print possible IPs
######################################################################
@@ -2309,7 +2249,6 @@ sub sendmail {
######################################################################
## split_by_comma
## default
-## minimum
## opt
######################################################################
sub split_by_comma {
@@ -2319,20 +2258,25 @@ sub split_by_comma {
return ();
}
sub default {
- my $v = shift;
+ my ($v, $h) = @_;
+ if (defined($h) && $config{$h}) {
+ my $proto = $protocols{opt('protocol', $v eq 'protocol' ? undef : $h)};
+ my $var = $proto->{variables}{$v} if $proto;
+ return $var->{default} if $var;
+ }
return undef if !defined($variables{'merged'}{$v});
+ # TODO: This might grab an arbitrary protocol-specific variable definition, which could cause
+ # surprising behavior.
return $variables{'merged'}{$v}{'default'};
}
-sub minimum {
- my $v = shift;
- return undef if !defined($variables{'merged'}{$v});
- return $variables{'merged'}{$v}{'minimum'};
-}
sub opt {
my $v = shift;
my $h = shift;
return $config{$h}{$v} if defined($h) && defined($config{$h}{$v});
- return $opt{$v} // $globals{$v} // default($v);
+ # TODO: Why check %opt before %globals? Valid variables from %opt are merged into %globals by
+ # init_config(), so it shouldn't be necessary. Also, it runs the risk of collision with a
+ # non-variable command line option like `--version`, `--help`, etc.
+ return $opt{$v} // $globals{$v} // default($v, $h);
}
sub min {
my $min = shift;
@@ -2480,14 +2424,13 @@ sub interval {
return $value;
}
sub interval_expired {
- my ($host, $time, $interval) = @_;
-
- return 0 if ($config{$host}{$interval} // 0) == 'inf';
+ my ($host, $time, $interval_opt) = @_;
+ my $interval = opt($interval_opt, $host);
+ return 0 if ($interval // 0) == 'inf';
return 1 if !exists $recap{$host};
return 1 if !exists $recap{$host}{$time} || !$recap{$host}{$time};
- return 1 if !exists $config{$host}{$interval} || !$config{$host}{$interval};
-
- return $now > ($recap{$host}{$time} + $config{$host}{$interval});
+ return 1 if !$interval;
+ return $now > ($recap{$host}{$time} + $interval);
}
@@ -2496,7 +2439,8 @@ sub interval_expired {
## check_value
######################################################################
sub check_value {
- my ($value, $def) = @_;
+ my ($orig, $def) = @_;
+ my $value = $orig;
my $type = $def->{'type'};
my $min = $def->{'minimum'};
my $required = $def->{'required'};
@@ -2506,14 +2450,14 @@ sub check_value {
} elsif (!defined($value) && $required) {
# None of the types have 'undef' as a valid value, so check definedness once here for
# convenience.
- return undef;
+ die("$type is required\n");
} elsif ($type eq T_DELAY) {
$value = interval($value);
$value = $min if defined($value) && defined($min) && $value < $min;
} elsif ($type eq T_NUMBER) {
- return undef if $value !~ /^\d+$/;
+ die("invalid $type: $orig\n") if $value !~ /^\d+$/;
$value = $min if defined($min) && $value < $min;
} elsif ($type eq T_BOOL) {
@@ -2522,55 +2466,62 @@ sub check_value {
} elsif ($value =~ /^(n(o)?|f(alse)?|0)$/i) {
$value = 0;
} else {
- return undef;
+ die("invalid $type: $orig\n");
}
- } elsif ($type eq T_FQDN || $type eq T_OFQDN && $value ne '') {
+ } elsif ($type eq T_FQDN) {
$value = lc $value;
- return undef if $value !~ /[^.]\.[^.]/;
+ die("invalid $type: $orig\n") if ($value ne '' || $required) && $value !~ /[^.]\.[^.]/;
} elsif ($type eq T_FQDNP) {
$value = lc $value;
- return undef if $value !~ /[^.]\.[^.].*(:\d+)?$/;
+ die("invalid $type: $orig\n") if $value !~ /[^.]\.[^.].*(:\d+)?$/;
} elsif ($type eq T_PROTO) {
$value = lc $value;
- return undef if !exists $protocols{$value};
+ die("invalid $type: $orig\nSupported values: ", join(' ', sort(keys(%protocols))), "\n")
+ if !exists $protocols{$value};
} elsif ($type eq T_URL) {
- return undef if $value !~ qr{^(?i:https?://)?[^./]+(\.[^./]+)+(:\d+)?(/[^/]+)*/?$};
+ die("invalid $type: $orig\n")
+ if $value !~ qr{^(?i:https?://)?[^./]+(\.[^./]+)+(:\d+)?(/[^/]+)*/?$};
} elsif ($type eq T_USE) {
$value = lc $value;
- return undef if !exists $ip_strategies{$value};
+ $value = 'disabled' if $value eq 'no'; # backwards compatibility
+ die(map(($_, "\n"), "invalid $type: $orig", 'Supported values:', ip_strategies_usage()))
+ if !exists($ip_strategies{$value});
} elsif ($type eq T_USEV4) {
$value = lc $value;
- return undef if !exists $ipv4_strategies{$value};
+ die(map(($_, "\n"), "invalid $type: $orig", 'Supported values:', ipv4_strategies_usage()))
+ if !exists($ipv4_strategies{$value});
} elsif ($type eq T_USEV6) {
$value = lc $value;
- return undef if !exists $ipv6_strategies{$value};
+ $value = 'disabled' if $value eq 'no'; # backwards compatibility
+ die(map(($_, "\n"), "invalid $type: $orig", 'Supported values:', ipv6_strategies_usage()))
+ if !exists($ipv6_strategies{$value});
} elsif ($type eq T_FILE) {
- return undef if $value eq "";
+ die("invalid $type: $orig\n") if $value eq "";
} elsif ($type eq T_IF) {
- return undef if $value !~ /^[a-zA-Z0-9:._-]+$/;
+ die("invalid $type: $orig\n") if $value !~ /^[a-zA-Z0-9:._-]+$/;
} elsif ($type eq T_PROG) {
- return undef if $value eq "";
+ die("invalid $type: $orig\n") if $value eq "";
} elsif ($type eq T_LOGIN) {
- return undef if $value eq "";
+ die("invalid $type: $orig\n") if $value eq "";
} elsif ($type eq T_IP) {
- return undef if !is_ipv4($value) && !is_ipv6($value);
+ die("invalid $type: $orig\n") if !is_ipv4($value) && !is_ipv6($value);
} elsif ($type eq T_IPV4) {
- return undef if !is_ipv4($value);
+ die("invalid $type: $orig\n") if !is_ipv4($value);
} elsif ($type eq T_IPV6) {
- return undef if !is_ipv6($value);
+ die("invalid $type: $orig\n") if !is_ipv6($value);
}
return $value;
@@ -2703,7 +2654,7 @@ sub geturl {
$use_ssl = 1;
} elsif ($url =~ /^http:/) {
$use_ssl = 0;
- } elsif ($globals{'ssl'} && !($params{ignore_ssl_option} // 0)) {
+ } elsif (opt('ssl') && !($params{ignore_ssl_option} // 0)) {
$use_ssl = 1;
} else {
$use_ssl = 0;
@@ -2777,9 +2728,7 @@ sub geturl {
## get_ip
######################################################################
sub get_ip {
- my $use = lc shift;
- $use = 'disabled' if ($use eq 'no'); # backward compatibility
- my $h = shift;
+ my ($use, $h) = @_;
my ($ip, $reply, $url, $skip) = (undef, '');
my $argvar = $use;
# Note that --use=firewallname uses --fw=arg, not --firewallname=arg.
@@ -3174,8 +3123,7 @@ sub get_ip_from_interface {
## get_ipv4
######################################################################
sub get_ipv4 {
- my $usev4 = lc(shift); ## Method to obtain IP address
- my $h = shift; ## Host/service making the request
+ my ($usev4, $h) = @_;
my $ipv4 = undef; ## Found IPv4 address
my $reply = ''; ## Text returned from various methods
my $url = ''; ## URL of website or firewall
@@ -3284,9 +3232,7 @@ sub get_ipv4 {
## get_ipv6
######################################################################
sub get_ipv6 {
- my $usev6 = lc(shift); ## Method to obtain IP address
- $usev6 = 'disabled' if ($usev6 eq 'no'); # backward compatibility
- my $h = shift; ## Host/service making the request
+ my ($usev6, $h) = @_;
my $ipv6 = undef; ## Found IPv6 address
my $reply = ''; ## Text returned from various methods
my $url = ''; ## URL of website or firewall
@@ -3383,7 +3329,7 @@ sub group_hosts_by {
my %groups;
my %cfgs;
for my $h (@$hosts) {
- my %cfg = map({ ($_ => $config{$h}{$_}); } grep(exists($config{$h}{$_}), @attrs));
+ my %cfg = map({ ($_ => opt($_, $h)); } grep(defined(opt($_, $h)), @attrs));
my $sig = repr(\%cfg, Indent => 0);
push(@{$groups{$sig}}, $h);
$cfgs{$sig} = \%cfg;
@@ -3491,31 +3437,28 @@ EoEXAMPLE
######################################################################
sub nic_updateable {
my ($host) = @_;
- my $force_update = $protocols{$config{$host}{protocol}}{force_update};
+ my $force_update = $protocols{opt('protocol', $host)}{force_update};
my $update = 0;
my $ip = $config{$host}{'wantip'};
my $ipv4 = $config{$host}{'wantipv4'};
my $ipv6 = $config{$host}{'wantipv6'};
- my $use = opt('use', $host) // 'disabled';
- my $usev4 = opt('usev4', $host) // 'disabled';
- my $usev6 = opt('usev6', $host) // 'disabled';
- $use = 'disabled' if ($use eq 'no'); # backward compatibility
- $usev6 = 'disabled' if ($usev6 eq 'no'); # backward compatibility
- $use = 'disabled' if ($usev4 ne 'disabled') || ($usev6 ne 'disabled');
+ my $use = opt('use', $host);
+ my $usev4 = opt('usev4', $host);
+ my $usev6 = opt('usev6', $host);
my $inv_ip_warn_count = opt('max-warn');
my $previp = $recap{$host}{'ip'} || '';
my $previpv4 = $recap{$host}{'ipv4'} || '';
my $previpv6 = $recap{$host}{'ipv6'} || '';
my %prettyt = map({ ($_ => $recap{$host}{$_} ? prettytime($recap{$host}{$_}) : ''); }
qw(atime mtime wtime));
- my %prettyi = map({ ($_ => prettyinterval($config{$host}{$_})); }
+ my %prettyi = map({ ($_ => prettyinterval(opt($_, $host))); }
qw(max-interval min-error-interval min-interval));
$warned_ip{$host} = 0 if $use ne 'disabled' && $ip;
$warned_ipv4{$host} = 0 if $usev4 ne 'disabled' && $ipv4;
$warned_ipv6{$host} = 0 if $usev6 ne 'disabled' && $ipv6;
- if ($opt{'force'}) {
+ if (opt('force')) {
info("update forced via 'force' option");
$update = 1;
@@ -3616,7 +3559,7 @@ sub nic_updateable {
} elsif (defined($force_update) && $force_update->($host)) {
$update = 1;
- } elsif (my @changed = grep({ my $rv = $recap{$host}{$_}; my $cv = $config{$host}{$_};
+ } elsif (my @changed = grep({ my $rv = $recap{$host}{$_}; my $cv = opt($_, $host);
defined($rv) && defined($cv) && $rv ne $cv; }
qw(static wildcard mx backupmx))) {
info("update forced because options changed: " . join(', ', @changed));
@@ -3745,22 +3688,22 @@ sub nic_dyndns1_update {
info("setting IP address to $ip");
my $url;
- $url = "https://$config{$h}{'server'}/nic/";
- $url .= ynu($config{$h}{'static'}, 'statdns', 'dyndns', 'dyndns');
+ $url = 'https://' . opt('server', $h) . '/nic/';
+ $url .= ynu(opt('static', $h), 'statdns', 'dyndns', 'dyndns');
$url .= "?action=edit&started=1&hostname=YES&host_id=$h";
$url .= "&myip=";
$url .= $ip if $ip;
- $url .= "&wildcard=ON" if ynu($config{$h}{'wildcard'}, 1, 0, 0);
- if ($config{$h}{'mx'}) {
- $url .= "&mx=$config{$h}{'mx'}";
- $url .= "&backmx=" . ynu($config{$h}{'backupmx'}, 'YES', 'NO');
+ $url .= "&wildcard=ON" if ynu(opt('wildcard', $h), 1, 0, 0);
+ if (opt('mx', $h)) {
+ $url .= '&mx=' . opt('mx', $h);
+ $url .= "&backmx=" . ynu(opt('backupmx', $h), 'YES', 'NO');
}
my $reply = geturl(
proxy => opt('proxy'),
url => $url,
- login => $config{$h}{'login'},
- password => $config{$h}{'password'},
+ login => opt('login', $h),
+ password => opt('password', $h),
);
next if !header_ok($reply);
@@ -3774,7 +3717,7 @@ sub nic_dyndns1_update {
if ($return_code ne 'NOERROR' || $error_code ne 'NOERROR' || !$title) {
$config{$h}{'status'} = 'failed';
- $title = "incomplete response from $config{$h}{server}" unless $title;
+ $title = 'incomplete response from ' . opt('server', $h) unless $title;
warning("SENT: %s", $url) unless opt('verbose');
warning("REPLIED: %s", $reply);
failed($title);
@@ -4006,7 +3949,7 @@ sub nic_dnsexit2_update {
# The DNSExit API does not support updating hosts with different zones at the same time,
# handling update per host.
for my $h (@_) {
- $config{$h}{'zone'} //= $h;
+ $config{$h}{'zone'} = $h if !defined(opt('zone', $h));
dnsexit2_update_host($h);
}
}
@@ -4018,10 +3961,11 @@ sub dnsexit2_update_host {
# Remove the zone suffix from $name. If the zone eq $name, $name can be left alone or
# set to the empty string; both have identical semantics. For consistency, always
# remove the zone even if it means $name becomes the empty string.
- if ($name =~ s/(?:^|\.)\Q$config{$h}{'zone'}\E$//) {
+ my $zone = opt('zone', $h);
+ if ($name =~ s/(?:^|\.)\Q$zone\E$//) {
# The zone was successfully trimmed from $name.
} else {
- fatal("hostname does not end with the zone: $config{$h}{'zone'}");
+ fatal("hostname does not end with the zone: " . opt('zone', $h));
}
# The IPv4 and IPv6 addresses must be updated together in a single API call.
my %ips;
@@ -4035,10 +3979,10 @@ sub dnsexit2_update_host {
name => $name,
type => ($ipv eq '6') ? 'AAAA' : 'A',
content => $ip,
- ttl => $config{$h}{'ttl'},
+ ttl => opt('ttl', $h),
});
};
- my $url = $config{$h}{'server'} . $config{$h}{'path'};
+ my $url = opt('server', $h) . opt('path', $h);
my $reply = geturl(
proxy => opt('proxy'),
url => $url,
@@ -4048,8 +3992,8 @@ sub dnsexit2_update_host {
],
method => 'POST',
data => encode_json({
- apikey => $config{$h}{'password'},
- domain => $config{$h}{'zone'},
+ apikey => opt('password', $h),
+ domain => $zone,
update => \@updates,
}),
);
@@ -4272,8 +4216,8 @@ sub nic_dslreports1_update {
info("setting IP address to $ip");
my $url;
- $url = "https://$config{$h}{'server'}/nic/";
- $url .= ynu($config{$h}{'static'}, 'statdns', 'dyndns', 'dyndns');
+ $url = 'https://' . opt('server', $h) . '/nic/';
+ $url .= ynu(opt('static', $h), 'statdns', 'dyndns', 'dyndns');
$url .= "?action=edit&started=1&hostname=YES&host_id=$h";
$url .= "&myip=";
$url .= $ip if $ip;
@@ -4281,11 +4225,11 @@ sub nic_dslreports1_update {
my $reply = geturl(
proxy => opt('proxy'),
url => $url,
- login => $config{$h}{'login'},
- password => $config{$h}{'password'},
+ login => opt('login', $h),
+ password => opt('password', $h),
) // '';
if ($reply eq '') {
- failed("request to $config{$h}{'server'} failed");
+ failed("request to " . opt('server', $h) . " failed");
next;
}
@@ -4346,9 +4290,9 @@ sub nic_domeneshop_update {
info("setting IPv$ipv address to $ip");
my $reply = geturl(
proxy => opt('proxy'),
- url => "$config{$h}{'server'}/v0/dyndns/update?hostname=$h&myip=$ip",
- login => $config{$h}{'login'},
- password => $config{$h}{'password'},
+ url => opt('server', $h) . "/v0/dyndns/update?hostname=$h&myip=$ip",
+ login => opt('login', $h),
+ password => opt('password', $h),
);
next if !header_ok($reply);
$config{$h}{"ipv$ipv"} = $ip;
@@ -4529,20 +4473,20 @@ sub nic_easydns_update {
my $ip = delete $config{$h}{"wantipv$ipv"} or next;
info("setting IPv$ipv address to $ip");
#'https://api.cp.easydns.com/dyn/generic.php?hostname=test.burry.ca&myip=10.20.30.40&wildcard=ON'
- my $url = "https://$config{$h}{'server'}$config{$h}{'script'}?hostname=$h&myip=$ip";
- $url .= "&wildcard=" . ynu($config{$h}{'wildcard'}, 'ON', 'OFF', 'OFF')
- if defined($config{$h}{'wildcard'});
- $url .= "&mx=$config{$h}{'mx'}&backmx=" . ynu($config{$h}{'backupmx'}, 'YES', 'NO')
- if $config{$h}{'mx'};
+ my $url = "https://" . opt('server', $h) . opt('script', $h) . "?hostname=$h&myip=$ip";
+ $url .= "&wildcard=" . ynu(opt('wildcard', $h), 'ON', 'OFF', 'OFF')
+ if defined(opt('wildcard', $h));
+ $url .= "&mx=" . opt('mx', $h) . "&backmx=" . ynu(opt('backupmx', $h), 'YES', 'NO')
+ if opt('mx', $h);
my $reply = geturl(
proxy => opt('proxy'),
url => $url,
- login => $config{$h}{'login'},
- password => $config{$h}{'password'},
+ login => opt('login', $h),
+ password => opt('password', $h),
);
next if !header_ok($reply);
(my $body = $reply) =~ s/^.*?\n\n//s or do {
- failed("Could not connect to $config{$h}{'server'}");
+ failed("could not connect to " . opt('server', $h));
next;
};
my $resultcode_re = join('|', map({quotemeta} 'NOERROR', keys(%errors)));
@@ -4613,13 +4557,13 @@ sub nic_namecheap_update {
info("setting IP address to $ip");
my $url;
- $url = "https://$config{$h}{'server'}/update";
- my $domain = $config{$h}{'login'};
+ $url = 'https://' . opt('server', $h) . '/update';
+ my $domain = opt('login', $h);
my $host = $h;
$host =~ s/(.*)\.$domain(.*)/$1$2/;
$url .= "?host=$host";
$url .= "&domain=$domain";
- $url .= "&password=$config{$h}{'password'}";
+ $url .= '&password=' . opt('password', $h);
$url .= "&ip=";
$url .= $ip if $ip;
@@ -4692,7 +4636,7 @@ sub nic_nfsn_gen_auth_header {
## 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'} . ';';
+ $auth_header .= opt('login', $h) . ';';
## timestamp is the standard 32-bit unsigned Unix timestamp
## value.
@@ -4710,10 +4654,10 @@ sub nic_nfsn_gen_auth_header {
## 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'} . ';' .
+ my $hash_string = opt('login', $h) . ';' .
$timestamp . ';' .
$salt . ';' .
- $config{$h}{'password'} . ';';
+ opt('password', $h) . ';';
## The request-uri value is the path portion of the requested URL
## (i.e. excluding the protocol and hostname).
@@ -4741,7 +4685,7 @@ sub nic_nfsn_make_request {
my $method = shift // 'GET';
my $body = shift // '';
- my $base_url = "https://$config{$h}{'server'}";
+ my $base_url = 'https://' . opt('server', $h);
my $url = $base_url . $path;
my $header = nic_nfsn_gen_auth_header($h, $path, $body);
if ($method eq 'POST' && $body ne '') {
@@ -4793,7 +4737,7 @@ sub nic_nfsn_update {
## update each configured host
for my $h (@_) {
local $_l = pushlogctx($h);
- my $zone = $config{$h}{'zone'};
+ my $zone = opt('zone', $h);
my $name;
if ($h eq $zone) {
@@ -4827,7 +4771,7 @@ sub nic_nfsn_update {
next;
}
- my $rr_ttl = $config{$h}{'ttl'};
+ my $rr_ttl = opt('ttl', $h);
if (ref($list) eq 'ARRAY' && defined $list->[0]->{'data'}) {
my $rr_data = $list->[0]->{'data'};
@@ -4908,11 +4852,11 @@ sub nic_njalla_update {
# Read input params
my $ipv4 = delete $config{$h}{'wantipv4'};
my $ipv6 = delete $config{$h}{'wantipv6'};
- my $quietreply = $config{$h}{'quietreply'};
+ my $quietreply = opt('quietreply', $h);
my $ip_output = '';
# Build url
- my $url = "https://$config{$h}{'server'}/update/?h=$h&k=$config{$h}{'password'}";
+ my $url = 'https://' . opt('server', $h) . "/update/?h=$h&k=" . opt('password', $h);
my $auto = 1;
for my $ip ($ipv4, $ipv6) {
next if (!$ip);
@@ -4949,7 +4893,7 @@ sub nic_njalla_update {
$response = eval {decode_json(${^MATCH})};
# No response, declare as failed
if (!defined($reply) || !$reply) {
- failed("could not connect to $config{$h}{'server'}");
+ failed("could not connect to " . opt('server', $h));
} else {
# Strip header
if ($response->{status} == 401 && $response->{message} =~ /invalid host or key/) {
@@ -5016,10 +4960,10 @@ sub nic_sitelutions_update {
info("setting IP address to $ip");
my $url;
- $url = "https://$config{$h}{'server'}/dnsup";
+ $url = 'https://' . opt('server', $h) . '/dnsup';
$url .= "?id=$h";
- $url .= "&user=$config{$h}{'login'}";
- $url .= "&pass=$config{$h}{'password'}";
+ $url .= '&user=' . opt('login', $h);
+ $url .= '&pass=' . opt('password', $h);
$url .= "&ip=";
$url .= $ip if $ip;
@@ -5102,8 +5046,8 @@ sub nic_freedns_update {
# address type.
my %recs_ipv4;
my %recs_ipv6;
- my $url_tmpl = "https://$config{$_[0]}{'server'}/api/?action=getdyndns&v=2&sha=";
- my $creds = sha1_hex("$config{$_[0]}{'login'}|$config{$_[0]}{'password'}");
+ my $url_tmpl = 'https://' . opt('server', $_[0]) . '/api/?action=getdyndns&v=2&sha=';
+ my $creds = sha1_hex(opt('login', $_[0]) . '|' . opt('password', $_[0]));
(my $url = $url_tmpl) =~ s//$creds/;
my $reply = geturl(proxy => opt('proxy'),
@@ -5228,8 +5172,8 @@ sub nic_1984_update {
info("setting IP address to $ip");
my $url;
- $url = "https://$config{$host}{'server'}/1.0/freedns/";
- $url .= "?apikey=$config{$host}{'password'}";
+ $url = 'https://' . opt('server', $host) . '/1.0/freedns/';
+ $url .= '?apikey=' . opt('password', $host);
$url .= "&domain=$host";
$url .= "&ip=$ip";
@@ -5304,7 +5248,7 @@ sub nic_changeip_update {
info("setting IP address to $ip");
my $url;
- $url = "https://$config{$h}{'server'}/nic/update";
+ $url = 'https://' . opt('server', $h) . '/nic/update';
$url .= "?hostname=$h";
$url .= "&ip=";
$url .= $ip if $ip;
@@ -5312,8 +5256,8 @@ sub nic_changeip_update {
my $reply = geturl(
proxy => opt('proxy'),
url => $url,
- login => $config{$h}{'login'},
- password => $config{$h}{'password'},
+ login => opt('login', $h),
+ password => opt('password', $h),
);
next if !header_ok($reply);
@@ -5376,31 +5320,31 @@ EoEXAMPLE
sub nic_godaddy_update {
for my $h (@_) {
local $_l = pushlogctx($h);
- my $zone = $config{$h}{'zone'};
+ my $zone = opt('zone', $h);
(my $hostname = $h) =~ s/\.\Q$zone\E$//;
for my $ipv ('4', '6') {
my $ip = delete($config{$h}{"wantipv$ipv"}) or next;
info("setting IPv$ipv address to $ip");
my $rrset_type = ($ipv eq '6') ? 'AAAA' : 'A';
- my $url = "https://$config{$h}{'server'}/$zone/records/$rrset_type/$hostname";
+ my $url = "https://" . opt('server', $h) . "/$zone/records/$rrset_type/$hostname";
my $reply = geturl(
proxy => opt('proxy'),
url => $url,
headers => [
'Content-Type: application/json',
'Accept: application/json',
- "Authorization: sso-key $config{$h}{'login'}:$config{$h}{'password'}",
+ "Authorization: sso-key " . opt('login', $h) . ":" . opt('password', $h),
],
method => 'PUT',
data => encode_json([{
data => $ip,
- defined($config{$h}{'ttl'}) ? (ttl => $config{$h}{'ttl'}) : (),
+ defined(opt('ttl', $h)) ? (ttl => opt('ttl', $h)) : (),
name => $hostname,
type => $rrset_type,
}]),
);
unless ($reply) {
- failed("could not connect to $config{$h}{'server'}");
+ failed("could not connect to " . opt('server', $h));
next;
}
(my $code) = ($reply =~ m%^s*HTTP/.*\s+(\d+)%i);
@@ -5418,7 +5362,7 @@ sub nic_godaddy_update {
if ($code eq "400") {
$msg = 'GoDaddy API URL ($url) was malformed.';
} elsif ($code eq "401") {
- if ($config{$h}{'login'} && $config{$h}{'login'}) {
+ if (opt('login', $h)) {
$msg = 'login or password option incorrect.';
} else {
$msg = 'login or password option missing.';
@@ -5493,9 +5437,9 @@ sub nic_henet_update {
info("setting IPv$ipv address to $ip");
my $reply = geturl(
proxy => opt('proxy'),
- url => "https://$config{$h}{'server'}/nic/update?hostname=$h&myip=$ip",
+ url => "https://" . opt('server', $h) . "/nic/update?hostname=$h&myip=$ip",
login => $h,
- password => $config{$h}{'password'},
+ password => opt('password', $h),
);
next if !header_ok($reply);
# dyn.dns.he.net can return 200 OK even if there is an error (e.g., bad authentication,
@@ -5576,10 +5520,10 @@ sub nic_mythicdyn_update {
info("Process configuration for IPV%s --------", $mythver);
my $reply = geturl(
proxy => opt('proxy'),
- url => "https://ipv$mythver.$config{$h}{'server'}/dns/v2/dynamic/$h",
+ url => "https://ipv$mythver." . opt('server', $h) . "/dns/v2/dynamic/$h",
method => 'POST',
- login => $config{$h}{'login'},
- password => $config{$h}{'password'},
+ login => opt('login', $h),
+ password => opt('password', $h),
ipversion => $mythver,
);
my $ok = header_ok($reply);
@@ -5678,7 +5622,7 @@ EoINSTR1
my $type = ($ip eq ($ipv6 // '')) ? 'AAAA' : 'A';
$instructions .= <<"EoINSTR2";
update delete $_. $type
-update add $_. $config{$_}{'ttl'} $type $ip
+update add $_. ${\(opt('ttl', $_))} $type $ip
EoINSTR2
}
}
@@ -5779,8 +5723,8 @@ sub nic_cloudflare_update {
info('getting Cloudflare Zone ID');
# Get zone ID
- my $url = "https://$config{$domain}{'server'}/zones/?";
- $url .= "name=" . $config{$domain}{'zone'};
+ my $url = "https://" . opt('server', $domain) . "/zones/?";
+ $url .= "name=" . opt('zone', $domain);
my $reply = geturl(proxy => opt('proxy'),
url => $url,
@@ -5796,9 +5740,9 @@ sub nic_cloudflare_update {
}
# Pull the ID out of the json, messy
- my ($zone_id) = map {$_->{name} eq $config{$domain}{'zone'} ? $_->{id} : ()} @{$response->{result}};
+ my ($zone_id) = map {$_->{name} eq opt('zone', $domain) ? $_->{id} : ()} @{$response->{result}};
unless ($zone_id) {
- failed("no zone ID found for zone $config{$domain}{'zone'}");
+ failed("no zone ID found for zone " . opt('zone', $domain));
next;
}
info("Zone ID is %s", $zone_id);
@@ -5814,7 +5758,7 @@ sub nic_cloudflare_update {
$config{$domain}{"status-ipv$ipv"} = 'failed';
# Get DNS 'A' or 'AAAA' record ID
- $url = "https://$config{$domain}{'server'}/zones/$zone_id/dns_records?";
+ $url = "https://" . opt('server', $domain) . "/zones/$zone_id/dns_records?";
$url .= "type=$type&name=$domain";
$reply = geturl(proxy => opt('proxy'),
url => $url,
@@ -5836,7 +5780,7 @@ sub nic_cloudflare_update {
}
debug("DNS '$type' record ID: $dns_rec_id");
# Set domain
- $url = "https://$config{$domain}{'server'}/zones/$zone_id/dns_records/$dns_rec_id";
+ $url = "https://" . opt('server', $domain) . "/zones/$zone_id/dns_records/$dns_rec_id";
my $data = "{\"content\":\"$ip\"}";
$reply = geturl(proxy => opt('proxy'),
url => $url,
@@ -5893,17 +5837,18 @@ EoEXAMPLE
sub nic_hetzner_update {
for my $domain (@_) {
local $_l = pushlogctx($domain);
- my $headers = "Auth-API-Token: $config{$domain}{'password'}\n";
+ my $headers = "Auth-API-Token: " . opt('password', $domain) . "\n";
$headers .= "Content-Type: application/json";
- (my $hostname = $domain) =~ s/\.$config{$domain}{zone}$//;
+ my $zone = opt('zone', $domain);
+ (my $hostname = $domain) =~ s/\Q.$zone\E$//;
my $ipv4 = delete $config{$domain}{'wantipv4'};
my $ipv6 = delete $config{$domain}{'wantipv6'};
info("getting Hetzner Zone ID");
# Get zone ID
- my $url = "https://$config{$domain}{'server'}/zones?name=" . $config{$domain}{'zone'};
+ my $url = "https://" . opt('server', $domain) . "/zones?name=$zone";
my $reply = geturl(proxy => opt('proxy'),
url => $url,
@@ -5919,9 +5864,9 @@ sub nic_hetzner_update {
}
# Pull the ID out of the json, messy
- my ($zone_id) = map {$_->{name} eq $config{$domain}{'zone'} ? $_->{id} : ()} @{$response->{zones}};
+ my ($zone_id) = map {$_->{name} eq $zone ? $_->{id} : ()} @{$response->{zones}};
unless ($zone_id) {
- failed("no zone ID found for zone $config{$domain}{'zone'}");
+ failed("no zone ID found for zone " . opt('zone', $domain));
next;
}
info("Zone ID is %s", $zone_id);
@@ -5936,7 +5881,7 @@ sub nic_hetzner_update {
$config{$domain}{"status-ipv$ipv"} = 'failed';
# Get DNS 'A' or 'AAAA' record ID
- $url = "https://$config{$domain}{'server'}/records?zone_id=$zone_id";
+ $url = "https://" . opt('server', $domain) . "/records?zone_id=$zone_id";
$reply = geturl(proxy => opt('proxy'),
url => $url,
headers => $headers
@@ -5957,14 +5902,14 @@ sub nic_hetzner_update {
if ($dns_rec_id)
{
debug("DNS '$type' record ID: $dns_rec_id");
- $url = "https://$config{$domain}{'server'}/records/$dns_rec_id";
+ $url = "https://" . opt('server', $domain) . "/records/$dns_rec_id";
$http_method = "PUT";
} else {
debug("creating DNS '$type'");
- $url = "https://$config{$domain}{'server'}/records";
+ $url = "https://" . opt('server', $domain) . "/records";
$http_method = "POST";
}
- my $data = "{\"zone_id\":\"$zone_id\", \"name\": \"$hostname\", \"value\": \"$ip\", \"type\": \"$type\", \"ttl\": $config{$domain}{'ttl'}}";
+ my $data = "{\"zone_id\":\"$zone_id\", \"name\": \"$hostname\", \"value\": \"$ip\", \"type\": \"$type\", \"ttl\": " . opt('ttl', $domain) . "}";
$reply = geturl(proxy => opt('proxy'),
url => $url,
@@ -6189,14 +6134,14 @@ sub nic_yandex_update {
for my $host (@_) {
local $_l = pushlogctx($host);
my $ip = delete $config{$host}{'wantip'};
- my $headers = "PddToken: $config{$host}{'password'}\n";
+ my $headers = "PddToken: " . opt('password', $host) . "\n";
info("setting IP address to $ip");
# Get record ID for host
- my $url = "https://$config{$host}{'server'}/api2/admin/dns/list?";
+ my $url = "https://" . opt('server', $host) . "/api2/admin/dns/list?";
$url .= "domain=";
- $url .= $config{$host}{'login'};
+ $url .= opt('login', $host);
my $reply = geturl(proxy => opt('proxy'), url => $url, headers => $headers);
next if !header_ok($reply);
@@ -6216,9 +6161,9 @@ sub nic_yandex_update {
}
# Update the DNS record
- $url = "https://$config{$host}{'server'}/api2/admin/dns/edit";
+ $url = "https://" . opt('server', $host) . "/api2/admin/dns/edit";
my $data = "domain=";
- $data .= $config{$host}{'login'};
+ $data .= opt('login', $host);
$data .= "&record_id=";
$data .= $id;
$data .= "&content=";
@@ -6353,7 +6298,7 @@ sub nic_freemyip_update {
local $_l = pushlogctx($h);
my $ip = delete $config{$h}{'wantip'};
info("setting IP address to $ip");
- my $url = "https://$config{$h}{'server'}/update?token=$config{$h}{'password'}&domain=$h";
+ my $url = "https://" . opt('server', $h) . "/update?token=" . opt('password', $h) . "&domain=$h";
my $reply = geturl(proxy => opt('proxy'), url => $url);
next if !header_ok($reply);
(my $body = $reply) =~ s/^.*?\n\n//s;
@@ -6408,7 +6353,7 @@ sub nic_ddnsfm_update {
info("setting IPv$ipv address to $ip");
my $reply = geturl(
proxy => opt('proxy'),
- url => "$config{$h}{server}/update?key=$config{$h}{password}&domain=$h&myip=$ip",
+ url => opt('server', $h) . "/update?key=" . opt('password', $h) . "&domain=$h&myip=$ip",
);
next if !header_ok($reply);
$config{$h}{"ipv$ipv"} = $ip;
@@ -6451,7 +6396,7 @@ sub nic_dondominio_update {
local $_l = pushlogctx($h);
my $ip = delete $config{$h}{'wantip'};
info("setting IP address to $ip");
- my $url = "https://$config{$h}{'server'}/plain/?user=$config{$h}{'login'}&password=$config{$h}{'password'}&host=$h&ip=$ip";
+ my $url = "https://" . opt('server', $h) . "/plain/?user=" . opt('login', $h) . "&password=" . opt('password', $h) . "&host=$h&ip=$ip";
my $reply = geturl(proxy => opt('proxy'), url => $url);
next if !header_ok($reply);
my @reply = split /\n/, $reply;
@@ -6514,7 +6459,7 @@ sub nic_dnsmadeeasy_update {
local $_l = pushlogctx($h);
my $ip = delete $config{$h}{'wantip'};
info("setting IP address to $ip");
- my $url = "$config{$h}{'server'}$config{$h}{'script'}?username=$config{$h}{'login'}&password=$config{$h}{'password'}&ip=$ip&id=$h";
+ my $url = opt('server', $h) . opt('script', $h) . "?username=" . opt('login', $h) . "&password=" . opt('password', $h) . "&ip=$ip&id=$h";
my $reply = geturl(proxy => opt('proxy'), url => $url);
next if !header_ok($reply);
my @reply = split /\n/, $reply;
@@ -6572,7 +6517,7 @@ sub nic_ovh_update {
# Set the URL that we're going to update
my $url;
- $url .= "https://$config{$h}{'server'}$config{$h}{'script'}?system=dyndns";
+ $url .= 'https://' . opt('server', $h) . opt('script', $h) . '?system=dyndns';
$url .= "&hostname=$h";
$url .= "&myip=";
$url .= $ip if $ip;
@@ -6580,12 +6525,12 @@ sub nic_ovh_update {
my $reply = geturl(
proxy => opt('proxy'),
url => $url,
- login => $config{$h}{'login'},
- password => $config{$h}{'password'},
+ login => opt('login', $h),
+ password => opt('password', $h),
);
if (!defined($reply) || !$reply) {
- failed("could not connect to $config{$h}{'server'}");
+ failed("could not connect to " . opt('server', $h));
next;
}
@@ -6687,16 +6632,16 @@ sub nic_porkbun_update {
for my $h (@_) {
local $_l = pushlogctx($h);
my ($sub_domain, $domain);
- if ($config{$h}{'root-domain'}) {
+ if (opt('root-domain', $h)) {
warning("both 'root-domain' and 'on-root-domain' are set; ignoring the latter")
- if $config{$h}{'on-root-domain'};
- $domain = $config{$h}{'root-domain'};
+ if opt('on-root-domain', $h);
+ $domain = opt('root-domain', $h);
$sub_domain = $h;
if ($sub_domain !~ s/(?:^|\.)\Q$domain\E$//) {
failed("hostname does not end with the 'root-domain' value: $domain");
next;
}
- } elsif ($config{$h}{'on-root-domain'}) {
+ } elsif (opt('on-root-domain', $h)) {
$sub_domain = '';
$domain = $h;
} else {
@@ -6713,8 +6658,8 @@ sub nic_porkbun_update {
headers => ['Content-Type: application/json'],
method => 'POST',
data => encode_json({
- secretapikey => $config{$h}{'secretapikey'},
- apikey => $config{$h}{'apikey'},
+ secretapikey => opt('secretapikey', $h),
+ apikey => opt('apikey', $h),
}),
);
next if !header_ok($reply);
@@ -6751,8 +6696,8 @@ sub nic_porkbun_update {
headers => ['Content-Type: application/json'],
method => 'POST',
data => encode_json({
- secretapikey => $config{$h}{'secretapikey'},
- apikey => $config{$h}{'apikey'},
+ secretapikey => opt('secretapikey', $h),
+ apikey => opt('apikey', $h),
content => $ip,
ttl => $ttl,
notes => $notes,
@@ -6859,15 +6804,15 @@ sub nic_dinahosting_update {
my $ip = delete $config{$h}{'wantip'};
info("setting IP address to $ip");
my ($hostname, $domain) = split(/\./, $h, 2);
- my $url = "https://$config{$h}{'server'}$config{$h}{'script'}";
+ my $url = 'https://' . opt('server', $h) . opt('script', $h);
$url .= "?hostname=$hostname";
$url .= "&domain=$domain";
$url .= "&command=Domain_Zone_UpdateType" . is_ipv6($ip) ? 'AAAA' : 'A';
$url .= "&ip=$ip";
my $reply = geturl(
proxy => opt('proxy'),
- login => $config{$h}{'login'},
- password => $config{$h}{'password'},
+ login => opt('login', $h),
+ password => opt('password', $h),
url => $url,
);
$config{$h}{'status'} = 'failed'; # assume failure until otherwise determined
@@ -6922,7 +6867,7 @@ sub nic_directnic_update {
my $ip = delete $config{$h}{"wantipv$ipv"} or next;
info("setting IPv$ipv address to $ip");
- my $url = $config{$h}{"urlv$ipv"};
+ my $url = opt("urlv$ipv", $h);
if (!defined($url)) {
failed("missing urlv$ipv option");
next;
@@ -7010,16 +6955,17 @@ sub nic_gandi_update {
local $_l = pushlogctx($h);
for my $ipv ('ipv4', 'ipv6') {
my $ip = delete $config{$h}{"want$ipv"} or next;
- (my $hostname = $h) =~ s/\.\Q$config{$h}{zone}\E$//;
+ my $zone = opt('zone', $h);
+ (my $hostname = $h) =~ s/\.\Q$zone\E$//;
info("setting IP address to $ip");
my @headers = ('Content-Type: application/json');
- if ($config{$h}{'use-personal-access-token'} == 1) {
- push(@headers, "Authorization: Bearer $config{$h}{'password'}");
+ if (opt('use-personal-access-token', $h) == 1) {
+ push(@headers, "Authorization: Bearer " . opt('password', $h));
} else {
- push(@headers, "Authorization: Apikey $config{$h}{'password'}");
+ push(@headers, "Authorization: Apikey " . opt('password', $h));
}
my $rrset_type = $ipv eq 'ipv6' ? 'AAAA' : 'A';
- my $url = "https://$config{$h}{'server'}$config{$h}{'script'}/livedns/domains/$config{$h}{'zone'}/records/$hostname/$rrset_type";
+ my $url = "https://" . opt('server', $h) . opt('script', $h) . "/livedns/domains/$zone/records/$hostname/$rrset_type";
my $reply = geturl(
proxy => opt('proxy'),
url => $url,
@@ -7034,8 +6980,8 @@ sub nic_gandi_update {
failed("response is not a JSON object: $reply");
next;
}
- if ($response->{'rrset_values'}->[0] eq $ip && (!defined($config{$h}{'ttl'}) ||
- $response->{'rrset_ttl'} eq $config{$h}{'ttl'})) {
+ if ($response->{'rrset_values'}->[0] eq $ip && (!defined(opt('ttl', $h)) ||
+ $response->{'rrset_ttl'} eq opt('ttl', $h))) {
$config{$h}{'ip'} = $ip;
$config{$h}{'mtime'} = $now;
$config{$h}{"status-$ipv"} = "good";
@@ -7048,7 +6994,7 @@ sub nic_gandi_update {
headers => \@headers,
method => 'PUT',
data => encode_json({
- defined($config{$h}{'ttl'}) ? (rrset_ttl => $config{$h}{'ttl'}) : (),
+ defined(opt('ttl', $h)) ? (rrset_ttl => opt('ttl', $h)) : (),
rrset_values => [$ip],
}),
);
@@ -7108,7 +7054,7 @@ sub nic_keysystems_update {
local $_l = pushlogctx($h);
my $ip = delete $config{$h}{'wantip'};
info("setting IP address to $ip");
- my $url = "$config{$h}{'server'}/update.php?hostname=$h&password=$config{$h}{'password'}&ip=$ip";
+ my $url = opt('server', $h) . "/update.php?hostname=$h&password=" . opt('password', $h) . "&ip=$ip";
my $reply = geturl(proxy => opt('proxy'), url => $url);
last if !header_ok($reply);
@@ -7156,7 +7102,7 @@ sub nic_regfishde_update {
my $ipv6 = delete $config{$h}{'wantipv6'};
info("setting IPv4 address to $ipv4") if $ipv4;
info("setting IPv6 address to $ipv6") if $ipv6;
- my $url = "https://$config{$h}{'server'}/?fqdn=$h&forcehost=1&token=$config{$h}{'password'}";
+ my $url = 'https://' . opt('server', $h) . "/?fqdn=$h&forcehost=1&token=" . opt('password', $h);
$url .= "&ipv4=$ipv4" if $ipv4;
$url .= "&ipv6=$ipv6" if $ipv6;
@@ -7225,10 +7171,10 @@ sub nic_enom_update {
info("setting IP address to $ip");
my $url;
- $url = "https://$config{$h}{'server'}/interface.asp?Command=SetDNSHost";
+ $url = 'https://' . opt('server', $h) . '/interface.asp?Command=SetDNSHost';
$url .= "&HostName=$h";
- $url .= "&Zone=$config{$h}{'login'}";
- $url .= "&DomainPassword=$config{$h}{'password'}";
+ $url .= '&Zone=' . opt('login', $h);
+ $url .= '&DomainPassword=' . opt('password', $h);
$url .= "&Address=";
$url .= $ip if $ip;
@@ -7288,15 +7234,15 @@ sub nic_digitalocean_update_one {
info("setting $ipv address to $ip");
- my $server = $config{$h}{'server'};
+ my $server = opt('server', $h);
my $type = $ipv eq 'ipv6' ? 'AAAA' : 'A';
my $headers;
$headers = "Content-Type: application/json\n";
- $headers .= "Authorization: Bearer $config{$h}{'password'}\n";
+ $headers .= 'Authorization: Bearer ' . opt('password', $h) . "\n";
my $list_url;
- $list_url = "https://$server/v2/domains/$config{$h}{'zone'}/records";
+ $list_url = "https://$server/v2/domains/" . opt('zone', $h) . '/records';
$list_url .= "?name=$h";
$list_url .= "&type=$type";
@@ -7333,7 +7279,7 @@ sub nic_digitalocean_update_one {
my $update_data = encode_json({'type' => $type, 'data' => $ip});
my $update_resp = geturl(
proxy => opt('proxy'),
- url => "https://$server/v2/domains/$config{$h}{'zone'}/records/$record_id",
+ url => "https://$server/v2/domains/" . opt('zone', $h) . "/records/$record_id",
method => 'PATCH',
headers => $headers,
data => $update_data,
@@ -7438,8 +7384,8 @@ sub nic_infomaniak_update {
my $reply = geturl(
proxy => opt('proxy'),
url => "https://infomaniak.com/nic/update?hostname=$h&myip=$ip",
- login => $config{$h}{'login'},
- password => $config{$h}{'password'},
+ login => opt('login', $h),
+ password => opt('password', $h),
);
next if !header_ok($reply);
(my $body = $reply) =~ s/^.*?\n\n//s;
@@ -7470,8 +7416,8 @@ sub nic_infomaniak_update {
## host must be specified; the host names are mentioned in the email.
######################################################################
sub nic_emailonly_update {
- # Note: This is logged after $config{$_}{'max-interval'] even if the IP address hasn't changed,
- # so it is best to avoid phrasing like, "IP address has changed."
+ # Note: This is logged after opt('max-interval', $_) even if the IP address hasn't changed, so
+ # it is best to avoid phrasing like, "IP address has changed."
logmsg(email => 1, raw => 1, join("\n", 'Host IP addresses:', map({
my $ipv4 = delete($config{$_}{'wantipv4'});
my $ipv6 = delete($config{$_}{'wantipv6'});
diff --git a/t/check_value.pl b/t/check_value.pl
index 02a0cde..d430851 100644
--- a/t/check_value.pl
+++ b/t/check_value.pl
@@ -12,7 +12,7 @@ my @test_cases = (
{
type => ddclient::T_FQDN(),
input => 'example',
- want => undef,
+ want_invalid => 1,
},
{
type => ddclient::T_URL(),
@@ -32,16 +32,22 @@ my @test_cases = (
{
type => ddclient::T_URL(),
input => 'ftp://bad.protocol/',
- want => undef,
+ want_invalid => 1,
},
{
type => ddclient::T_URL(),
input => 'bad-url',
- want => undef,
+ want_invalid => 1,
},
);
for my $tc (@test_cases) {
- my $got = ddclient::check_value($tc->{input}, ddclient::setv($tc->{type}, 0, 0, undef, undef));
- is($got, $tc->{want}, "$tc->{type}: $tc->{input}");
+ my $got;
+ my $got_invalid = !(eval {
+ $got = ddclient::check_value($tc->{input},
+ ddclient::setv($tc->{type}, 0, 0, undef, undef));
+ 1;
+ });
+ is($got_invalid, !!$tc->{want_invalid}, "$tc->{type}: $tc->{input}: validity");
+ is($got, $tc->{want}, "$tc->{type}: $tc->{input}: normalization") if !$tc->{want_invalid};
}
done_testing();
diff --git a/t/group_hosts_by.pl b/t/group_hosts_by.pl
index 4e2c29f..4cf25a1 100644
--- a/t/group_hosts_by.pl
+++ b/t/group_hosts_by.pl
@@ -72,7 +72,9 @@ my @test_cases = (
want => [
{cfg => {falsy => 0}, hosts => [$h1]},
{cfg => {falsy => ''}, hosts => [$h2]},
- {cfg => {falsy => undef}, hosts => [$h3]},
+ # undef intentionally becomes unset because undef always means "fall back to global or
+ # default".
+ {cfg => {}, hosts => [$h3]},
],
},
{
@@ -80,8 +82,9 @@ my @test_cases = (
groupby => [qw(maybeunset)],
want => [
{cfg => {maybeunset => 'unique'}, hosts => [$h1]},
- {cfg => {maybeunset => undef}, hosts => [$h2]},
- {cfg => {}, hosts => [$h3]},
+ # undef intentionally becomes unset because undef always means "fall back to global or
+ # default".
+ {cfg => {}, hosts => [$h2, $h3]},
],
},
{
diff --git a/t/parse_assignments.pl b/t/parse_assignments.pl
index d595459..ab965b9 100644
--- a/t/parse_assignments.pl
+++ b/t/parse_assignments.pl
@@ -44,8 +44,20 @@ my @test_cases = (
tc('unquoted escaped backslash', "a=\\\\", { a => "\\" }, ""),
tc('squoted escaped squote', "a='\\''", { a => "'" }, ""),
tc('dquoted escaped dquote', "a=\"\\\"\"", { a => '"' }, ""),
+ tc('env: empty', "a_env=", {}, ""),
+ tc('env: unset', "a_env=UNSET", {}, ""),
+ tc('env: set', "a_env=TEST", { a => 'val' }, ""),
+ tc('env: single quoted', "a_env='TEST'", { a => 'val' }, ""),
+ tc('newline: quoted value', "a='1\n2'", { a => "1\n2" }, ""),
+ tc('newline: escaped value', "a=1\\\n2", { a => "1\n2" }, ""),
+ tc('newline: between vars', "a=1 \n b=2", { a => '1' }, "\n b=2"),
+ tc('newline: terminating', "a=1 \n", { a => '1' }, "\n"),
);
+delete($ENV{''});
+delete($ENV{UNSET});
+$ENV{TEST} = 'val';
+
for my $tc (@test_cases) {
my ($got_rest, %got_vars) = ddclient::parse_assignments($tc->{input});
subtest $tc->{name} => sub {
diff --git a/t/protocol_dnsexit2.pl b/t/protocol_dnsexit2.pl
index b32c688..ec5137d 100644
--- a/t/protocol_dnsexit2.pl
+++ b/t/protocol_dnsexit2.pl
@@ -34,6 +34,8 @@ $httpd->run(sub {
diag(sprintf("started IPv4 server running at %s", $httpd->endpoint()));
+local $ddclient::globals{verbose} = 1;
+
my $ua = LWP::UserAgent->new;
sub test_nic_dnsexit2_update {
@@ -66,8 +68,6 @@ sub get_requests {
subtest 'Testing nic_dnsexit2_update' => sub {
my %config = (
'host.my.zone.com' => {
- 'ssl' => 'no',
- 'verbose' => 'yes',
'usev4' => 'ipv4',
'wantipv4' => '8.8.4.4',
'usev6' => 'ipv6',
@@ -75,7 +75,7 @@ subtest 'Testing nic_dnsexit2_update' => sub {
'protocol' => 'dnsexit2',
'password' => 'mytestingpassword',
'zone' => 'my.zone.com',
- 'server' => $httpd->host_port(),
+ 'server' => $httpd->endpoint(),
'path' => '/update',
'ttl' => 5
});
@@ -111,13 +111,11 @@ subtest 'Testing nic_dnsexit2_update' => sub {
subtest 'Testing nic_dnsexit2_update without a zone set' => sub {
my %config = (
'myhost.zone.com' => {
- 'ssl' => 'yes',
- 'verbose' => 'yes',
'usev4' => 'ipv4',
'wantipv4' => '8.8.4.4',
'protocol' => 'dnsexit2',
'password' => 'anotherpassword',
- 'server' => $httpd->host_port(),
+ 'server' => $httpd->endpoint(),
'path' => '/update-alt',
'ttl' => 10
});
@@ -143,24 +141,20 @@ subtest 'Testing nic_dnsexit2_update without a zone set' => sub {
subtest 'Testing nic_dnsexit2_update with two hostnames, one with a zone and one without' => sub {
my %config = (
'host1.zone.com' => {
- 'ssl' => 'yes',
- 'verbose' => 'yes',
'usev4' => 'ipv4',
'wantipv4' => '8.8.4.4',
'protocol' => 'dnsexit2',
'password' => 'testingpassword',
- 'server' => $httpd->host_port(),
+ 'server' => $httpd->endpoint(),
'path' => '/update',
'ttl' => 5
},
'host2.zone.com' => {
- 'ssl' => 'yes',
- 'verbose' => 'yes',
'usev6' => 'ipv6',
'wantipv6' => '2001:4860:4860::8888',
'protocol' => 'dnsexit2',
'password' => 'testingpassword',
- 'server' => $httpd->host_port(),
+ 'server' => $httpd->endpoint(),
'path' => '/update',
'ttl' => 10,
'zone' => 'zone.com'