diff --git a/Makefile.am b/Makefile.am index a1cef03..265cea1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -70,6 +70,8 @@ AM_PL_LOG_FLAGS = -Mstrict -w \ -I'$(abs_top_srcdir)'/t/lib \ -MDevel::Autoflush handwritten_tests = \ + t/is-and-extract-ipv4.pl \ + t/is-and-extract-ipv6.pl \ t/geturl_connectivity.pl \ t/geturl_ssl.pl \ t/parse_assignments.pl \ diff --git a/configure.ac b/configure.ac index 94b62b3..6151e88 100644 --- a/configure.ac +++ b/configure.ac @@ -49,6 +49,7 @@ m4_foreach_w([_m], [ # Perl modules required for tests. If these modules are not installed # then some tests will fail. Only prints a warning if not installed. m4_foreach_w([_m], [ + B Data::Dumper File::Spec::Functions File::Temp diff --git a/ddclient.in b/ddclient.in index 8b8a4a9..1cec317 100755 --- a/ddclient.in +++ b/ddclient.in @@ -2273,65 +2273,95 @@ sub get_ip { return $ip; } + ###################################################################### -## is_ipv4() validates if string is valid IPv4 address and only a -## valid address with no preceding or trailing spaces/characters -## and no embedded leading zeros. +## Regex to find IPv4 address. Accepts embedded leading zeros. +###################################################################### +my $regex_ipv4 = qr/(?:(?25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?&octet)/; + +###################################################################### +## is_ipv4() validates if string is valid IPv4 address with no preceding +## or trailing spaces/characters, not even line breaks. ###################################################################### sub is_ipv4 { - my ($value) = @_; - return (length($value // '') != 0) && ($value eq (extract_ipv4($value) // '')); + return (shift // '') =~ /\A$regex_ipv4\z/; } ###################################################################### -## extract_ipv4() extracts the first valid IPv4 address from given string. -## Accepts leading zeros in the address but removes them in returned value +## extract_ipv4() finds the first valid IPv4 address in the given string, +## removes embedded leading zeros, and returns the result. ###################################################################### sub extract_ipv4 { - (shift // '') =~ /\b((?:(?25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?&octet))\b/ - or return undef; + (shift // '') =~ /(?:\b|_)($regex_ipv4)(?:\b|_)/ or return undef; (my $ip = $1) =~ s/\b0+\B//g; ## remove embedded leading zeros return $ip; } +###################################################################### +## Regex that matches an IPv6 address. Accepts embedded leading zeros. +## Accepts IPv4-mapped IPv6 addresses such as 64:ff9b::192.0.2.13. +###################################################################### +my $regex_ipv6 = qr/ + # Define some named groups so we can use Perl's recursive subpattern feature for shorthand: + (?[0-9A-F]{1,4}){0} # "g" matches a group of 1 to 4 hex chars + (?(?&g):){0} # "g_" matches a group of 1 to 4 hex chars followed by a colon + (?<_g>:(?&g)){0} # "_g" matches a colon followed by a group of 1 to 4 hex chars + (?(?&g)?){0} # "g0" is an optional "g" (matches a group of 0 to 4 hex chars) + (?(?&g0):){0} # "g0_" is an optional "g" followed by a colon + (?[:.0-9A-Z]){0} # "x" matches chars that should never come before or after the address + (?$regex_ipv4){0} # "ip4" matches an IPv4 address x.x.x.x + + # Now for the regex itself: + (? sub { + foreach my $ip (@valid_ipv4) { + ok(ddclient::is_ipv4($ip), "is_ipv4('$ip')"); + } +}; + +subtest "is_ipv4() with invalid addresses" => sub { + foreach my $ip (@invalid_ipv4) { + ok(!ddclient::is_ipv4($ip), sprintf("!is_ipv4(%s)", defined($ip) ? "'$ip'" : 'undef')); + } +}; + +subtest "is_ipv4() with char adjacent to valid address" => sub { + foreach my $ch (split(//, '/.,:z @$#&%!^*()_-+'), "\n") { + subtest perlstring($ch) => sub { + foreach my $ip (@valid_ipv4) { + subtest $ip => sub { + my $test = $ch . $ip; # insert at front + ok(!ddclient::is_ipv4($test), "!is_ipv4('$test')"); + $test = $ip . $ch; # add at end + ok(!ddclient::is_ipv4($test), "!is_ipv4('$test')"); + $test = $ch . $ip . $ch; # wrap front and end + ok(!ddclient::is_ipv4($test), "!is_ipv4('$test')"); + }; + } + }; + } +}; + +subtest "extract_ipv4()" => sub { + my @test_cases = ( + {name => "undef", text => undef, want => undef}, + {name => "empty", text => "", want => undef}, + {name => "invalid", text => "1.2.3.256", want => undef}, + {name => "two addrs", text => "1.1.1.1\n2.2.2.2", want => "1.1.1.1"}, + {name => "host+port", text => "1.2.3.4:123", want => "1.2.3.4"}, + {name => "zero pad", text => "001.002.003.004", want => "1.2.3.4"}, + ); + foreach my $tc (@test_cases) { + is(ddclient::extract_ipv4($tc->{text}), $tc->{want}, $tc->{name}); + } +}; + +subtest "extract_ipv4() of valid addr with adjacent non-word char" => sub { + foreach my $wb (split(//, '/, @$#&%!^*()_-+:'), "\n") { + subtest perlstring($wb) => sub { + my $test = ""; + foreach my $ip (@valid_ipv4) { + $test = "foo" . $wb . $ip . $wb . "bar"; # wrap front and end + $ip =~ s/\b0+\B//g; ## remove embedded leading zeros for testing + is(ddclient::extract_ipv4($test), $ip, perlstring($test)); + } + }; + } +}; + +done_testing(); diff --git a/t/is-and-extract-ipv6.pl b/t/is-and-extract-ipv6.pl new file mode 100644 index 0000000..bb33bc9 --- /dev/null +++ b/t/is-and-extract-ipv6.pl @@ -0,0 +1,492 @@ +use Test::More; +use B qw(perlstring); + +SKIP: { eval { require Test::Warnings; } or skip($@, 1); } +eval { require 'ddclient'; } or BAIL_OUT($@); + + +my @valid_ipv6 = ( + "::abcd:efAB:CDEF", # case sensitivity + "08:09:0a:0b:0c:0d:0e:0f", # leading zeros + + # with thanks to http://home.deds.nl/~aeron/regex/valid_ipv6.txt + "1111:2222:3333:4444:5555:6666:7777:8888", + "1111:2222:3333:4444:5555:6666:7777::", + "1111:2222:3333:4444:5555:6666::", + "1111:2222:3333:4444:5555::", + "1111:2222:3333:4444::", + "1111:2222:3333::", + "1111:2222::", + "1111::", + "::", + "1111:2222:3333:4444:5555:6666::8888", + "1111:2222:3333:4444:5555::8888", + "1111:2222:3333:4444::8888", + "1111:2222:3333::8888", + "1111:2222::8888", + "1111::8888", + "::8888", + "1111:2222:3333:4444:5555::7777:8888", + "1111:2222:3333:4444::7777:8888", + "1111:2222:3333::7777:8888", + "1111:2222::7777:8888", + "1111::7777:8888", + "::7777:8888", + "1111:2222:3333:4444::6666:7777:8888", + "1111:2222:3333::6666:7777:8888", + "1111:2222::6666:7777:8888", + "1111::6666:7777:8888", + "::6666:7777:8888", + "1111:2222:3333::5555:6666:7777:8888", + "1111:2222::5555:6666:7777:8888", + "1111::5555:6666:7777:8888", + "::5555:6666:7777:8888", + "1111:2222::4444:5555:6666:7777:8888", + "1111::4444:5555:6666:7777:8888", + "::4444:5555:6666:7777:8888", + "1111::3333:4444:5555:6666:7777:8888", + "::3333:4444:5555:6666:7777:8888", + "::2222:3333:4444:5555:6666:7777:8888", + # IPv4-mapped IPv6 addresses + "1111:2222:3333:4444:5555:6666:0.0.0.0", + "1111:2222:3333:4444:5555:6666:00.00.00.00", + "1111:2222:3333:4444:5555:6666:000.000.000.000", + "1111:2222:3333:4444:5555:6666:123.123.123.123", + "1111:2222:3333:4444:5555::123.123.123.123", + "1111:2222:3333:4444::123.123.123.123", + "1111:2222:3333::123.123.123.123", + "1111:2222::123.123.123.123", + "1111::123.123.123.123", + "::123.123.123.123", + "1111:2222:3333:4444::6666:123.123.123.123", + "1111:2222:3333::6666:123.123.123.123", + "1111:2222::6666:123.123.123.123", + "1111::6666:123.123.123.123", + "::6666:123.123.123.123", + "1111:2222:3333::5555:6666:123.123.123.123", + "1111:2222::5555:6666:123.123.123.123", + "1111::5555:6666:123.123.123.123", + "::5555:6666:123.123.123.123", + "1111:2222::4444:5555:6666:123.123.123.123", + "1111::4444:5555:6666:123.123.123.123", + "::4444:5555:6666:123.123.123.123", + "1111::3333:4444:5555:6666:123.123.123.123", + "::3333:4444:5555:6666:123.123.123.123", + "::2222:3333:4444:5555:6666:123.123.123.123", +); + +my @invalid_ipv6 = ( + # Empty string and bogus text + undef, + "", + " ", + "foobar", + + # Valid IPv6 with extra text before or after + "foo2001:DB8:4341:0781:1111:2222:3333:4444", + "foo 2001:DB8:4341:0781::4444", + "foo 2001:DB8:4341:0781:1111:: bar", + "foo2001:DB8:4341:0781::100bar", + "2001:DB8:4341:0781::1 bar", + "2001:DB8:4341:0781::0001bar", + "foo bar 3001:DB8:4341:0781:1111:2222:3333:4444 foo bar", + "__3001:DB8:4341:0781::4444", + "__3001:DB8:4341:0781:1111::__", + "--3001:DB8:4341:0781::100--", + "/3001:DB8:4341:0781::1/", + "3001:DB8:4341:0781::0001%", + "fdb6:1d86:d9bd:1::4444%eth0", + "fdb6:1d86:d9bd:1:1111::%ens192", + "fdb6:1d86:d9bd:1::100%en0", + "fdb6:1d86:d9bd:1::1%eth1.100", + + # With thanks to http://home.deds.nl/~aeron/regex/invalid_ipv6.txt + # Invalid data + "XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX", + + # Too many components + "1111:2222:3333:4444:5555:6666:7777:8888:9999", + "1111:2222:3333:4444:5555:6666:7777:8888::", + "::2222:3333:4444:5555:6666:7777:8888:9999", + + # Too few components + "1111:2222:3333:4444:5555:6666:7777", + "1111:2222:3333:4444:5555:6666", + "1111:2222:3333:4444:5555", + "1111:2222:3333:4444", + "1111:2222:3333", + "1111:2222", + "1111", + + # Missing : + "11112222:3333:4444:5555:6666:7777:8888", + "1111:22223333:4444:5555:6666:7777:8888", + "1111:2222:33334444:5555:6666:7777:8888", + "1111:2222:3333:44445555:6666:7777:8888", + "1111:2222:3333:4444:55556666:7777:8888", + "1111:2222:3333:4444:5555:66667777:8888", + "1111:2222:3333:4444:5555:6666:77778888", + + # Missing : intended for :: + "1111:2222:3333:4444:5555:6666:7777:8888:", + "1111:2222:3333:4444:5555:6666:7777:", + "1111:2222:3333:4444:5555:6666:", + "1111:2222:3333:4444:5555:", + "1111:2222:3333:4444:", + "1111:2222:3333:", + "1111:2222:", + "1111:", + ":", + ":8888", + ":7777:8888", + ":6666:7777:8888", + ":5555:6666:7777:8888", + ":4444:5555:6666:7777:8888", + ":3333:4444:5555:6666:7777:8888", + ":2222:3333:4444:5555:6666:7777:8888", + ":1111:2222:3333:4444:5555:6666:7777:8888", + + # ::: + ":::2222:3333:4444:5555:6666:7777:8888", + "1111:::3333:4444:5555:6666:7777:8888", + "1111:2222:::4444:5555:6666:7777:8888", + "1111:2222:3333:::5555:6666:7777:8888", + "1111:2222:3333:4444:::6666:7777:8888", + "1111:2222:3333:4444:5555:::7777:8888", + "1111:2222:3333:4444:5555:6666:::8888", + "1111:2222:3333:4444:5555:6666:7777:::", + + # Double :: + "::2222::4444:5555:6666:7777:8888", + "::2222:3333::5555:6666:7777:8888", + "::2222:3333:4444::6666:7777:8888", + "::2222:3333:4444:5555::7777:8888", + "::2222:3333:4444:5555:7777::8888", + "::2222:3333:4444:5555:7777:8888::", + + "1111::3333::5555:6666:7777:8888", + "1111::3333:4444::6666:7777:8888", + "1111::3333:4444:5555::7777:8888", + "1111::3333:4444:5555:6666::8888", + "1111::3333:4444:5555:6666:7777::", + + "1111:2222::4444::6666:7777:8888", + "1111:2222::4444:5555::7777:8888", + "1111:2222::4444:5555:6666::8888", + "1111:2222::4444:5555:6666:7777::", + + "1111:2222:3333::5555::7777:8888", + "1111:2222:3333::5555:6666::8888", + "1111:2222:3333::5555:6666:7777::", + + "1111:2222:3333:4444::6666::8888", + "1111:2222:3333:4444::6666:7777::", + + "1111:2222:3333:4444:5555::7777::", + + # Invalid data + "XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:1.2.3.4", + "1111:2222:3333:4444:5555:6666:256.256.256.256", + + # Too many components + "1111:2222:3333:4444:5555:6666:7777:8888:1.2.3", + "1111:2222:3333:4444:5555:6666:7777:1.2.3.4", + "1111:2222:3333:4444:5555:6666::1.2.3.4", + "::2222:3333:4444:5555:6666:7777:1.2.3.4", + "1111:2222:3333:4444:5555:6666:1.2.3.4.5", + + # Too few components + "1111:2222:3333:4444:5555:1.2.3.4", + "1111:2222:3333:4444:1.2.3.4", + "1111:2222:3333:1.2.3.4", + "1111:2222:1.2.3.4", + "1111:1.2.3.4", + "1.2.3.4", + + # Missing : + "11112222:3333:4444:5555:6666:1.2.3.4", + "1111:22223333:4444:5555:6666:1.2.3.4", + "1111:2222:33334444:5555:6666:1.2.3.4", + "1111:2222:3333:44445555:6666:1.2.3.4", + "1111:2222:3333:4444:55556666:1.2.3.4", + "1111:2222:3333:4444:5555:66661.2.3.4", + + # Missing . + "1111:2222:3333:4444:5555:6666:255255.255.255", + "1111:2222:3333:4444:5555:6666:255.255255.255", + "1111:2222:3333:4444:5555:6666:255.255.255255", + + # Missing : intended for :: + ":1.2.3.4", + ":6666:1.2.3.4", + ":5555:6666:1.2.3.4", + ":4444:5555:6666:1.2.3.4", + ":3333:4444:5555:6666:1.2.3.4", + ":2222:3333:4444:5555:6666:1.2.3.4", + ":1111:2222:3333:4444:5555:6666:1.2.3.4", + + # ::: + ":::2222:3333:4444:5555:6666:1.2.3.4", + "1111:::3333:4444:5555:6666:1.2.3.4", + "1111:2222:::4444:5555:6666:1.2.3.4", + "1111:2222:3333:::5555:6666:1.2.3.4", + "1111:2222:3333:4444:::6666:1.2.3.4", + "1111:2222:3333:4444:5555:::1.2.3.4", + + # Double :: + "::2222::4444:5555:6666:1.2.3.4", + "::2222:3333::5555:6666:1.2.3.4", + "::2222:3333:4444::6666:1.2.3.4", + "::2222:3333:4444:5555::1.2.3.4", + + "1111::3333::5555:6666:1.2.3.4", + "1111::3333:4444::6666:1.2.3.4", + "1111::3333:4444:5555::1.2.3.4", + + "1111:2222::4444::6666:1.2.3.4", + "1111:2222::4444:5555::1.2.3.4", + + "1111:2222:3333::5555::1.2.3.4", + + # Missing parts + "::.", + "::..", + "::...", + "::1...", + "::1.2..", + "::1.2.3.", + "::.2..", + "::.2.3.", + "::.2.3.4", + "::..3.", + "::..3.4", + "::...4", + + # Extra : in front + ":1111:2222:3333:4444:5555:6666:7777::", + ":1111:2222:3333:4444:5555:6666::", + ":1111:2222:3333:4444:5555::", + ":1111:2222:3333:4444::", + ":1111:2222:3333::", + ":1111:2222::", + ":1111::", + ":::", + ":1111:2222:3333:4444:5555:6666::8888", + ":1111:2222:3333:4444:5555::8888", + ":1111:2222:3333:4444::8888", + ":1111:2222:3333::8888", + ":1111:2222::8888", + ":1111::8888", + ":::8888", + ":1111:2222:3333:4444:5555::7777:8888", + ":1111:2222:3333:4444::7777:8888", + ":1111:2222:3333::7777:8888", + ":1111:2222::7777:8888", + ":1111::7777:8888", + ":::7777:8888", + ":1111:2222:3333:4444::6666:7777:8888", + ":1111:2222:3333::6666:7777:8888", + ":1111:2222::6666:7777:8888", + ":1111::6666:7777:8888", + ":::6666:7777:8888", + ":1111:2222:3333::5555:6666:7777:8888", + ":1111:2222::5555:6666:7777:8888", + ":1111::5555:6666:7777:8888", + ":::5555:6666:7777:8888", + ":1111:2222::4444:5555:6666:7777:8888", + ":1111::4444:5555:6666:7777:8888", + ":::4444:5555:6666:7777:8888", + ":1111::3333:4444:5555:6666:7777:8888", + ":::3333:4444:5555:6666:7777:8888", + ":::2222:3333:4444:5555:6666:7777:8888", + ":1111:2222:3333:4444:5555:6666:1.2.3.4", + ":1111:2222:3333:4444:5555::1.2.3.4", + ":1111:2222:3333:4444::1.2.3.4", + ":1111:2222:3333::1.2.3.4", + ":1111:2222::1.2.3.4", + ":1111::1.2.3.4", + ":::1.2.3.4", + ":1111:2222:3333:4444::6666:1.2.3.4", + ":1111:2222:3333::6666:1.2.3.4", + ":1111:2222::6666:1.2.3.4", + ":1111::6666:1.2.3.4", + ":::6666:1.2.3.4", + ":1111:2222:3333::5555:6666:1.2.3.4", + ":1111:2222::5555:6666:1.2.3.4", + ":1111::5555:6666:1.2.3.4", + ":::5555:6666:1.2.3.4", + ":1111:2222::4444:5555:6666:1.2.3.4", + ":1111::4444:5555:6666:1.2.3.4", + ":::4444:5555:6666:1.2.3.4", + ":1111::3333:4444:5555:6666:1.2.3.4", + ":::3333:4444:5555:6666:1.2.3.4", + ":::2222:3333:4444:5555:6666:1.2.3.4", + + # Extra : at end + "1111:2222:3333:4444:5555:6666:7777:::", + "1111:2222:3333:4444:5555:6666:::", + "1111:2222:3333:4444:5555:::", + "1111:2222:3333:4444:::", + "1111:2222:3333:::", + "1111:2222:::", + "1111:::", + ":::", + "1111:2222:3333:4444:5555:6666::8888:", + "1111:2222:3333:4444:5555::8888:", + "1111:2222:3333:4444::8888:", + "1111:2222:3333::8888:", + "1111:2222::8888:", + "1111::8888:", + "::8888:", + "1111:2222:3333:4444:5555::7777:8888:", + "1111:2222:3333:4444::7777:8888:", + "1111:2222:3333::7777:8888:", + "1111:2222::7777:8888:", + "1111::7777:8888:", + "::7777:8888:", + "1111:2222:3333:4444::6666:7777:8888:", + "1111:2222:3333::6666:7777:8888:", + "1111:2222::6666:7777:8888:", + "1111::6666:7777:8888:", + "::6666:7777:8888:", + "1111:2222:3333::5555:6666:7777:8888:", + "1111:2222::5555:6666:7777:8888:", + "1111::5555:6666:7777:8888:", + "::5555:6666:7777:8888:", + "1111:2222::4444:5555:6666:7777:8888:", + "1111::4444:5555:6666:7777:8888:", + "::4444:5555:6666:7777:8888:", + "1111::3333:4444:5555:6666:7777:8888:", + "::3333:4444:5555:6666:7777:8888:", + "::2222:3333:4444:5555:6666:7777:8888:", +); + +my @if_samples = ( + # Sample output from: + # ip -6 -o addr show dev scope global + # This seems to be consistent accross platforms. The last line is from Ubuntu of a static + # assigned IPv6. + ["ip -6 -o addr show dev scope global", <<'EOF'], +2: ens160 inet6 fdb6:1d86:d9bd:1::8214/128 scope global dynamic noprefixroute \ valid_lft 63197sec preferred_lft 63197sec +2: ens160 inet6 2001:DB8:4341:0781::8214/128 scope global dynamic noprefixroute \ valid_lft 63197sec preferred_lft 63197sec +2: ens160 inet6 2001:DB8:4341:0781:89b9:4b1c:186c:a0c7/64 scope global temporary dynamic \ valid_lft 85954sec preferred_lft 21767sec +2: ens160 inet6 fdb6:1d86:d9bd:1:89b9:4b1c:186c:a0c7/64 scope global temporary dynamic \ valid_lft 85954sec preferred_lft 21767sec +2: ens160 inet6 fdb6:1d86:d9bd:1:34a6:c329:c52e:8ba6/64 scope global temporary deprecated dynamic \ valid_lft 85954sec preferred_lft 0sec +2: ens160 inet6 fdb6:1d86:d9bd:1:b417:fe35:166b:4816/64 scope global dynamic mngtmpaddr noprefixroute \ valid_lft 85954sec preferred_lft 85954sec +2: ens160 inet6 2001:DB8:4341:0781:34a6:c329:c52e:8ba6/64 scope global temporary deprecated dynamic \ valid_lft 85954sec preferred_lft 0sec +2: ens160 inet6 2001:DB8:4341:0781:f911:a224:7e69:d22/64 scope global dynamic mngtmpaddr noprefixroute \ valid_lft 85954sec preferred_lft 85954sec +2: ens160 inet6 2001:DB8:4341:0781::100/128 scope global noprefixroute \ valid_lft forever preferred_lft forever +EOF + # Sample output from MacOS: + # ifconfig | grep -w "inet6" + # (Yes, there is a tab at start of each line.) The last two lines are with a manually + # configured static GUA. + ["MacOS: ifconfig | grep -w \"inet6\"", <<'EOF'], + inet6 fe80::1419:abd0:5943:8bbb%en0 prefixlen 64 secured scopeid 0xa + inet6 fdb6:1d86:d9bd:1:142c:8e9e:de48:843e prefixlen 64 autoconf secured + inet6 fdb6:1d86:d9bd:1:7447:cf67:edbd:cea4 prefixlen 64 autoconf temporary + inet6 fdb6:1d86:d9bd:1::c5b3 prefixlen 64 dynamic + inet6 2001:DB8:4341:0781:141d:66b9:2ba1:b67d prefixlen 64 autoconf secured + inet6 2001:DB8:4341:0781:64e1:b68f:e8af:5d6e prefixlen 64 autoconf temporary + inet6 fe80::1419:abd0:5943:8bbb%en0 prefixlen 64 secured scopeid 0xa + inet6 2001:DB8:4341:0781::101 prefixlen 64 +EOF + ["RHEL: ifconfig | grep -w \"inet6\"", <<'EOF'], + inet6 2001:DB8:4341:0781::dc14 prefixlen 128 scopeid 0x0 + inet6 fe80::cd48:4a58:3b0f:4d30 prefixlen 64 scopeid 0x20 + inet6 2001:DB8:4341:0781:e720:3aec:a936:36d4 prefixlen 64 scopeid 0x0 + inet6 fdb6:1d86:d9bd:1:9c16:8cbf:ae33:f1cc prefixlen 64 scopeid 0x0 + inet6 fdb6:1d86:d9bd:1::dc14 prefixlen 128 scopeid 0x0 +EOF + ["Ubuntu: ifconfig | grep -w \"inet6\"", <<'EOF'], + inet6 fdb6:1d86:d9bd:1:34a6:c329:c52e:8ba6 prefixlen 64 scopeid 0x0 + inet6 fdb6:1d86:d9bd:1:89b9:4b1c:186c:a0c7 prefixlen 64 scopeid 0x0 + inet6 fdb6:1d86:d9bd:1::8214 prefixlen 128 scopeid 0x0 + inet6 fdb6:1d86:d9bd:1:b417:fe35:166b:4816 prefixlen 64 scopeid 0x0 + inet6 fe80::5b31:fc63:d353:da68 prefixlen 64 scopeid 0x20 + inet6 2001:DB8:4341:0781::8214 prefixlen 128 scopeid 0x0 + inet6 2001:DB8:4341:0781:34a6:c329:c52e:8ba6 prefixlen 64 scopeid 0x0 + inet6 2001:DB8:4341:0781:89b9:4b1c:186c:a0c7 prefixlen 64 scopeid 0x0 + inet6 2001:DB8:4341:0781:f911:a224:7e69:d22 prefixlen 64 scopeid 0x0 +EOF + ["Busybox: ifconfig | grep -w \"inet6\"", <<'EOF'], + inet6 addr: fe80::4362:31ff:fe08:61b4/64 Scope:Link + inet6 addr: 2001:DB8:4341:0781:ed44:eb63:b070:212f/128 Scope:Global +EOF +); + + +subtest "is_ipv6() with valid addresses" => sub { + foreach my $ip (@valid_ipv6) { + ok(ddclient::is_ipv6($ip), "is_ipv6('$ip')"); + } +}; + +subtest "is_ipv6() with invalid addresses" => sub { + foreach my $ip (@invalid_ipv6) { + ok(!ddclient::is_ipv6($ip), sprintf("!is_ipv6(%s)", defined($ip) ? "'$ip'" : 'undef')); + } +}; + +subtest "is_ipv6() with char adjacent to valid address" => sub { + foreach my $ch (split(//, '/.,:z @$#&%!^*()_-+'), "\n") { + subtest perlstring($ch) => sub { + foreach my $ip (@valid_ipv6) { + subtest $ip => sub { + my $test = $ch . $ip; # insert at front + ok(!ddclient::is_ipv6($test), "!is_ipv6('$test')"); + $test = $ip . $ch; # add at end + ok(!ddclient::is_ipv6($test), "!is_ipv6('$test')"); + $test = $ch . $ip . $ch; # wrap front and end + ok(!ddclient::is_ipv6($test), "!is_ipv6('$test')"); + }; + } + }; + } +}; + +subtest "extract_ipv6()" => sub { + my @test_cases = ( + {name => "undef", text => undef, want => undef}, + {name => "empty", text => "", want => undef}, + {name => "invalid", text => "::12345", want => undef}, + {name => "two addrs", text => "::1\n::2", want => "::1"}, + {name => "zone index", text => "fe80::1%0", want => "fe80::1"}, + {name => "url host+port", text => "[::1]:123", want => "::1"}, + {name => "url host+zi+port", text => "[fe80::1%250]:123", want => "fe80::1"}, + {name => "zero pad", text => "::0001", want => "::1"}, + ); + foreach my $tc (@test_cases) { + is(ddclient::extract_ipv6($tc->{text}), $tc->{want}, $tc->{name}); + } +}; + +subtest "extract_ipv6() of valid addr with adjacent non-word char" => sub { + foreach my $wb (split(//, '/, @$#&%!^*()_-+'), "\n") { + subtest perlstring($wb) => sub { + my $test = ""; + foreach my $ip (@valid_ipv6) { + $test = "foo" . $wb . $ip . $wb . "bar"; # wrap front and end + $ip =~ s/\b0+\B//g; ## remove embedded leading zeros for testing + is(ddclient::extract_ipv6($test), $ip, perlstring($test)); + } + }; + } +}; + +subtest "interface config samples" => sub { + for my $sample (@if_samples) { + my ($name, $text) = @$sample; + subtest $name => sub { + my $ip = ddclient::extract_ipv6($text); + ok(ddclient::is_ipv6($ip), "extract_ipv6(\$text) returns an IPv6 address"); + foreach my $line (split(/\n/, $text)) { + my $ip = ddclient::extract_ipv6($line); + ok(ddclient::is_ipv6($ip), + sprintf("extract_ipv6(%s) returns an IPv6 address", perlstring($line))); + } + } + } +}; + +done_testing();