September 16, 2008
-#
-# Support for multiple domain support for Namecheap by Robert Ian Hawdon 2010-09-03: https://robertianhawdon.me.uk/
-#
-# Initial Cloudflare support by Ian Pye, updated by Robert Ian Hawdon 2012-07-16
-# Further updates by Peter Roberts to support the new API 2013-09-26, 2014-06-22: http://blog.peter-r.co.uk/
-#
-#
-######################################################################
-require 5.004;
-use strict;
-use Getopt::Long;
-use Sys::Hostname;
-use IO::Socket;
-use Data::Validate::IP;
-
-my $version = "3.9.1";
-my $programd = $0;
-$programd =~ s%^.*/%%;
-my $program = $programd;
-$program =~ s/d$//;
-my $now = time;
-my $hostname = hostname();
-my $etc = ($program =~ /test/i) ? './' : '/etc/ddclient/';
-my $cachedir = ($program =~ /test/i) ? './' : '/var/cache/ddclient/';
-my $savedir = ($program =~ /test/i) ? 'URL/' : '/tmp/';
-my $msgs = '';
-my $last_msgs = '';
-
-use vars qw($file $lineno);
-local $file = '';
-local $lineno = '';
-
-$ENV{'PATH'} = (exists($ENV{PATH}) ? "$ENV{PATH}:" : "") . "/sbin:/usr/sbin:/bin:/usr/bin:/etc:/usr/lib:";
-
-sub T_ANY {'any'};
-sub T_STRING {'string'};
-sub T_EMAIL {'e-mail address'};
-sub T_NUMBER {'number'};
-sub T_DELAY {'time delay (ie. 1d, 1hour, 1m)'};
-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'}
-sub T_USE {'ip strategy'}
-sub T_IF {'interface'}
-sub T_PROG {'program name'}
-sub T_IP {'ip'}
-sub T_POSTS {'postscript'};
-
-## strategies for obtaining an ip address.
-my %builtinweb = (
- 'dyndns' => { 'url' => 'http://checkip.dyndns.org/', 'skip' =>
- 'Current IP Address:', },
- 'dnspark' => { 'url' => 'http://ipdetect.dnspark.com/', 'skip' => 'Current Address:', },
- 'loopia' => { 'url' => 'http://dns.loopia.se/checkip/checkip.php', 'skip' => 'Current IP Address:', },
-);
-my %builtinfw = (
- 'watchguard-soho' => {
- 'name' => 'Watchguard SOHO FW',
- 'url' => '/pubnet.htm',
- 'skip' => 'NAME=IPAddress VALUE=',
- },
- 'netopia-r910' => {
- 'name' => 'Netopia R910 FW',
- 'url' => '/WanEvtLog',
- 'skip' => 'local:',
- },
- 'smc-barricade' => {
- 'name' => 'SMC Barricade FW',
- 'url' => '/status.htm',
- 'skip' => 'IP Address',
- },
- 'smc-barricade-alt' => {
- 'name' => 'SMC Barricade FW (alternate config)',
- 'url' => '/status.HTM',
- 'skip' => 'WAN IP',
- },
- 'smc-barricade-7401bra' => {
- 'name' => 'SMC Barricade 7401BRA FW',
- 'url' => '/admin/wan1.htm',
- 'skip' => 'IP Address',
- },
- 'netgear-rt3xx' => {
- 'name' => 'Netgear FW',
- 'url' => '/mtenSysStatus.html',
- 'skip' => 'IP Address',
- },
- 'elsa-lancom-dsl10' => {
- 'name' => 'ELSA LanCom DSL/10 DSL FW',
- 'url' => '/config/1/6/8/3/',
- 'skip' => 'IP.Address',
- },
- 'elsa-lancom-dsl10-ch01' => {
- 'name' => 'ELSA LanCom DSL/10 DSL FW (isdn ch01)',
- 'url' => '/config/1/6/8/3/',
- 'skip' => 'IP.Address.*?CH01',
- },
- 'elsa-lancom-dsl10-ch02' => {
- 'name' => 'ELSA LanCom DSL/10 DSL FW (isdn ch01)',
- 'url' => '/config/1/6/8/3/',
- 'skip' => 'IP.Address.*?CH02',
- },
- 'linksys' => {
- 'name' => 'Linksys FW',
- 'url' => '/Status.htm',
- 'skip' => 'WAN.*?Address',
- },
- 'linksys-ver2' => {
- 'name' => 'Linksys FW version 2',
- 'url' => '/RouterStatus.htm',
- 'skip' => 'WAN.*?Address',
- },
- 'linksys-ver3' => {
- 'name' => 'Linksys FW version 3',
- 'url' => '/Status_Router.htm',
- 'skip' => 'WAN.*?Address',
- },
- 'linksys-wrt854g' => {
- 'name' => 'Linksys WRT854G FW',
- 'url' => '/Status_Router.asp',
- 'skip' => 'IP Address:',
- },
- 'maxgate-ugate3x00' => {
- 'name' => 'MaxGate UGATE-3x00 FW',
- 'url' => '/Status.htm',
- 'skip' => 'WAN.*?IP Address',
- },
- 'netcomm-nb3' => {
- 'name' => 'NetComm NB3',
- 'url' => '/MainPage?id=6',
- 'skip' => 'ppp-0',
- },
- '3com-3c886a' => {
- 'name' => '3com 3c886a 56k Lan Modem',
- 'url' => '/stat3.htm',
- 'skip' => 'IP address in use',
- },
- 'sohoware-nbg800' => {
- 'name' => 'SOHOWare BroadGuard NBG800',
- 'url' => '/status.htm',
- 'skip' => 'Internet IP',
- },
- 'xsense-aero' => {
- 'name' => 'Xsense Aero',
- 'url' => '/A_SysInfo.htm',
- 'skip' => 'WAN.*?IP Address',
- },
- 'alcatel-stp' => {
- 'name' => 'Alcatel Speed Touch Pro',
- 'url' => '/cgi/router/',
- 'skip' => 'Brt',
- },
- 'alcatel-510' => {
- 'name' => 'Alcatel Speed Touch 510',
- 'url' => '/cgi/ip/',
- 'skip' => 'ppp',
- },
- 'allnet-1298' => {
- 'name' => 'Allnet 1298',
- 'url' => '/cgi/router/',
- 'skip' => 'WAN',
- },
- '3com-oc-remote812' => {
- 'name' => '3com OfficeConnect Remote 812',
- 'url' => '/callEvent',
- 'skip' => '.*LOCAL',
- },
- 'e-tech' => {
- 'name' => 'E-tech Router',
- 'url' => '/Status.htm',
- 'skip' => 'Public IP Address',
- },
- 'cayman-3220h' => {
- 'name' => 'Cayman 3220-H DSL',
- 'url' => '/shell/show+ip+interfaces',
- 'skip' => '.*inet',
- },
- 'vigor-2200usb' => {
- 'name' => 'Vigor 2200 USB',
- 'url' => '/doc/online.sht',
- 'skip' => 'PPPoA',
- },
- 'dlink-614' => {
- 'name' => 'D-Link DI-614+',
- 'url' => '/st_devic.html',
- 'skip' => 'WAN',
- },
- 'dlink-604' => {
- 'name' => 'D-Link DI-604',
- 'url' => '/st_devic.html',
- 'skip' => 'WAN.*?IP.*Address',
- },
- 'olitec-SX200' => {
- 'name' => 'olitec-SX200',
- 'url' => '/doc/wan.htm',
- 'skip' => 'st_wan_ip[0] = "',
- },
- 'westell-6100' => {
- 'name' => 'Westell C90-610015-06 DSL Router',
- 'url' => '/advstat.htm',
- 'skip' => 'IP.+?Address',
- },
- '2wire' => {
- 'name' => '2Wire 1701HG Gateway',
- 'url' => '/xslt?PAGE=B01',
- 'skip' => 'Internet Address:',
- },
- 'linksys-rv042-wan1' => {
- 'name' => 'Linksys RV042 Dual Homed Router WAN Port 2',
- 'url' => '/home.htm',
- 'skip' => 'WAN1 IP',
- },
- 'linksys-rv042-wan2' => {
- 'name' => 'Linksys RV042 Dual Homed Router WAN Port 2',
- 'url' => '/home.htm',
- 'skip' => 'WAN2 IP',
- },
- 'netgear-rp614' => {
- 'name' => 'Netgear RP614 FW',
- 'url' => '/sysstatus.html',
- 'skip' => 'IP Address',
- },
- 'watchguard-edge-x' => {
- 'name' => 'Watchguard Edge X FW',
- 'url' => '/netstat.htm',
- 'skip' => 'inet addr:',
- },
- 'dlink-524' => {
- 'name' => 'D-Link DI-524',
- 'url' => '/st_device.html',
- 'skip' => 'WAN.*?Addres',
- },
- 'rtp300' => {
- 'name' => 'Linksys RTP300',
- 'url' => '/cgi-bin/webcm?getpage=%2Fusr%2Fwww_safe%2Fhtml%2Fstatus%2FRouter.html',
- 'skip' => 'Internet.*?IP Address',
- },
- 'netgear-wpn824' => {
- 'name' => 'Netgear WPN824 FW',
- 'url' => '/RST_status.htm',
- 'skip' => 'IP Address',
- },
- 'linksys-wcg200' => {
- 'name' => 'Linksys WCG200 FW',
- 'url' => '/RgStatus.asp',
- 'skip' => 'WAN.IP.*?Address',
- },
- 'netgear-dg834g' => {
- 'name' => 'netgear-dg834g',
- 'url' => '/setup.cgi?next_file=s_status.htm&todo=cfg_init',
- 'skip' => '',
- },
- 'netgear-wgt624' => {
- 'name' => 'Netgear WGT624',
- 'url' => '/RST_st_dhcp.htm',
- 'skip' => 'IP Address',
- },
- 'sveasoft' => {
- 'name' => 'Sveasoft WRT54G/WRT54GS',
- 'url' => '/Status_Router.asp',
- 'skip' => 'var wan_ip',
- },
- 'smc-barricade-7004vbr' => {
- 'name' => 'SMC Barricade FW (7004VBR model config)',
- 'url' => '/status_main.stm',
- 'skip' => 'var wan_ip=',
- },
- 'sitecom-dc202' => {
- 'name' => 'Sitecom DC-202 FW',
- 'url' => '/status.htm',
- 'skip' => 'Internet IP Address',
- },
-);
-my %ip_strategies = (
- 'ip' => ": obtain IP from -ip {address}",
- 'web' => ": obtain IP from an IP discovery page on the web",
- 'fw' => ": obtain IP from the firewall specified by -fw {type|address}",
- 'if' => ": obtain IP from the -if {interface}",
- 'cmd' => ": obtain IP from the -cmd {external-command}",
- 'cisco' => ": obtain IP from Cisco FW at the -fw {address}",
- 'cisco-asa' => ": obtain IP from Cisco ASA at the -fw {address}",
- map { $_ => sprintf ": obtain IP from %s at the -fw {address}", $builtinfw{$_}->{'name'} } keys %builtinfw,
-);
-sub ip_strategies_usage {
- return map { sprintf(" -use=%-22s %s.", $_, $ip_strategies{$_}) } sort keys %ip_strategies;
-}
-
-my %web_strategies = (
- 'dyndns'=> 1,
- 'dnspark'=> 1,
- 'loopia'=> 1,
-);
-
-sub setv {
- return {
- 'type' => shift,
- 'required' => shift,
- 'cache' => shift,
- 'config' => shift,
- 'default' => shift,
- 'minimum' => shift,
- };
-};
-my %variables = (
- 'global-defaults' => {
- 'daemon' => setv(T_DELAY, 0, 0, 1, 0, interval('60s')),
- 'foreground' => setv(T_BOOL, 0, 0, 1, 0, undef),
- 'file' => setv(T_FILE, 0, 0, 1, "$etc$program.conf", undef),
- 'cache' => setv(T_FILE, 0, 0, 1, "$cachedir$program.cache", undef),
- 'pid' => setv(T_FILE, 0, 0, 1, "", undef),
- 'proxy' => setv(T_FQDNP, 0, 0, 1, '', undef),
- 'protocol' => setv(T_PROTO, 0, 0, 1, 'dyndns2', undef),
-
- 'use' => setv(T_USE, 0, 0, 1, 'ip', undef),
- 'ip' => setv(T_IP, 0, 0, 1, undef, undef),
- 'if' => setv(T_IF, 0, 0, 1, 'ppp0', undef),
- 'if-skip' => setv(T_STRING,1, 0, 1, '', undef),
- 'web' => setv(T_STRING,0, 0, 1, 'dyndns', undef),
- 'web-skip' => setv(T_STRING,1, 0, 1, '', undef),
- 'fw' => setv(T_ANY, 0, 0, 1, '', undef),
- 'fw-skip' => setv(T_STRING,1, 0, 1, '', undef),
- 'fw-banlocal' => setv(T_BOOL, 0, 0, 1, 0, undef),
- 'fw-login' => setv(T_LOGIN, 1, 0, 1, '', undef),
- 'fw-password' => setv(T_PASSWD,1, 0, 1, '', undef),
- 'cmd' => setv(T_PROG, 0, 0, 1, '', undef),
- 'cmd-skip' => setv(T_STRING,1, 0, 1, '', undef),
-
- 'timeout' => setv(T_DELAY, 0, 0, 1, interval('120s'), interval('120s')),
- 'retry' => setv(T_BOOL, 0, 0, 0, 0, undef),
- 'force' => setv(T_BOOL, 0, 0, 0, 0, undef),
- 'ssl' => setv(T_BOOL, 0, 0, 0, 0, undef),
- 'ipv6' => setv(T_BOOL, 0, 0, 0, 0, undef),
- 'syslog' => setv(T_BOOL, 0, 0, 1, 0, undef),
- 'facility' => setv(T_STRING,0, 0, 1, 'daemon', undef),
- 'priority' => setv(T_STRING,0, 0, 1, 'notice', undef),
- 'mail' => setv(T_EMAIL, 0, 0, 1, '', undef),
- 'mail-failure' => setv(T_EMAIL, 0, 0, 1, '', undef),
-
- 'exec' => setv(T_BOOL, 0, 0, 1, 1, undef),
- 'debug' => setv(T_BOOL, 0, 0, 1, 0, undef),
- 'verbose' => setv(T_BOOL, 0, 0, 1, 0, undef),
- 'quiet' => setv(T_BOOL, 0, 0, 1, 0, undef),
- 'help' => setv(T_BOOL, 0, 0, 1, 0, undef),
- 'test' => setv(T_BOOL, 0, 0, 1, 0, undef),
- 'geturl' => setv(T_STRING,0, 0, 0, '', undef),
-
- 'postscript' => setv(T_POSTS, 0, 0, 1, '', undef),
- },
- 'service-common-defaults' => {
- 'server' => setv(T_FQDNP, 1, 0, 1, 'members.dyndns.org', undef),
- 'login' => setv(T_LOGIN, 1, 0, 1, '', undef),
- 'password' => setv(T_PASSWD, 1, 0, 1, '', undef),
- 'host' => setv(T_STRING, 1, 1, 1, '', undef),
-
- 'use' => setv(T_USE, 0, 0, 1, 'ip', undef),
- 'if' => setv(T_IF, 0, 0, 1, 'ppp0', undef),
- 'if-skip' => setv(T_STRING,0, 0, 1, '', undef),
- 'web' => setv(T_STRING,0, 0, 1, 'dyndns', undef),
- 'web-skip' => setv(T_STRING,0, 0, 1, '', undef),
- 'fw' => setv(T_ANY, 0, 0, 1, '', undef),
- 'fw-skip' => setv(T_STRING,0, 0, 1, '', undef),
- 'fw-banlocal' => setv(T_BOOL, 0, 0, 1, 0, undef),
- 'fw-login' => setv(T_LOGIN, 0, 0, 1, '', undef),
- 'fw-password' => setv(T_PASSWD,0, 0, 1, '', undef),
- 'cmd' => setv(T_PROG, 0, 0, 1, '', undef),
- 'cmd-skip' => setv(T_STRING,0, 0, 1, '', undef),
- 'ipv6' => setv(T_BOOL, 0, 0, 0, 0, undef),
- 'ip' => setv(T_IP, 0, 1, 0, undef, undef),
- 'wtime' => setv(T_DELAY, 0, 1, 1, 0, interval('30s')),
- 'mtime' => setv(T_NUMBER, 0, 1, 0, 0, undef),
- 'atime' => setv(T_NUMBER, 0, 1, 0, 0, undef),
- 'status' => setv(T_ANY, 0, 1, 0, '', undef),
- 'min-interval' => setv(T_DELAY, 0, 0, 1, interval('30s'), 0),
- 'max-interval' => setv(T_DELAY, 0, 0, 1, interval('25d'), 0),
- 'min-error-interval' => setv(T_DELAY, 0, 0, 1, interval('5m'), 0),
-
- 'warned-min-interval' => setv(T_ANY, 0, 1, 0, 0, undef),
- 'warned-min-error-interval' => setv(T_ANY, 0, 1, 0, 0, undef),
- },
- 'dyndns-common-defaults' => {
- 'static' => setv(T_BOOL, 0, 1, 1, 0, undef),
- 'wildcard' => setv(T_BOOL, 0, 1, 1, 0, undef),
- 'mx' => setv(T_OFQDN, 0, 1, 1, '', undef),
- 'backupmx' => setv(T_BOOL, 0, 1, 1, 0, undef),
- },
- 'easydns-common-defaults' => {
- 'wildcard' => setv(T_BOOL, 0, 1, 1, 0, undef),
- 'mx' => setv(T_OFQDN, 0, 1, 1, '', undef),
- 'backupmx' => setv(T_BOOL, 0, 1, 1, 0, undef),
- },
- 'dnspark-common-defaults' => {
- 'mx' => setv(T_OFQDN, 0, 1, 1, '', undef),
- 'mxpri' => setv(T_NUMBER, 0, 0, 1, 5, undef),
- },
- 'noip-common-defaults' => {
- 'static' => setv(T_BOOL, 0, 1, 1, 0, undef),
- },
- 'noip-service-common-defaults' => {
- 'server' => setv(T_FQDNP, 1, 0, 1, 'dynupdate.no-ip.com', undef),
- 'login' => setv(T_LOGIN, 1, 0, 1, '', undef),
- 'password' => setv(T_PASSWD, 1, 0, 1, '', undef),
- 'host' => setv(T_STRING, 1, 1, 1, '', undef),
- 'ip' => setv(T_IP, 0, 1, 0, undef, undef),
- 'wtime' => setv(T_DELAY, 0, 1, 1, 0, interval('30s')),
- 'mtime' => setv(T_NUMBER, 0, 1, 0, 0, undef),
- 'atime' => setv(T_NUMBER, 0, 1, 0, 0, undef),
- 'status' => setv(T_ANY, 0, 1, 0, '', undef),
- 'min-interval' => setv(T_DELAY, 0, 0, 1, interval('30s'), 0),
- 'max-interval' => setv(T_DELAY, 0, 0, 1, interval('25d'), 0),
- 'min-error-interval' => setv(T_DELAY, 0, 0, 1, interval('5m'), 0),
- 'warned-min-interval' => setv(T_ANY, 0, 1, 0, 0, undef),
- 'warned-min-error-interval' => setv(T_ANY, 0, 1, 0, 0, undef),
- },
- 'zoneedit-service-common-defaults' => {
- 'zone' => setv(T_OFQDN, 0, 0, 1, undef, undef),
- },
- 'dtdns-common-defaults' => {
- 'login' => setv(T_LOGIN, 0, 0, 0, 'unused', undef),
- 'client' => setv(T_STRING, 0, 1, 1, $program, undef),
- },
- 'nsupdate-common-defaults' => {
- 'ttl' => setv(T_NUMBER, 0, 1, 0, 600, undef),
- 'zone' => setv(T_STRING, 1, 1, 1, '', undef),
- 'tcp' => setv(T_BOOL, 0, 1, 1, 0, undef),
- },
- 'cloudflare-common-defaults' => {
- 'server' => setv(T_FQDNP, 1, 0, 1, 'api.cloudflare.com/client/v4', undef),
- 'zone' => setv(T_FQDN, 1, 0, 1, '', undef),
- 'static' => setv(T_BOOL, 0, 1, 1, 0, undef),
- 'wildcard' => setv(T_BOOL, 0, 1, 1, 0, undef),
- 'mx' => setv(T_OFQDN, 0, 1, 1, '', undef),
- 'backupmx' => setv(T_BOOL, 0, 1, 1, 0, undef),
- 'ttl' => setv(T_NUMBER, 1, 0, 1, 1, undef),
- },
- 'googledomains-common-defaults' => {
- 'server' => setv(T_FQDNP, 1, 0, 1, 'domains.google.com', undef),
- },
- 'duckdns-common-defaults' => {
- 'server' => setv(T_FQDNP, 1, 0, 1, 'www.duckdns.org', undef),
- 'login' => setv(T_LOGIN, 0, 0, 0, 'unused', undef),
- },
- 'freemyip-common-defaults' => {
- 'server' => setv(T_FQDNP, 1, 0, 1, 'freemyip.com', undef),
- 'login' => setv(T_LOGIN, 0, 0, 0, 'unused', undef),
- },
- 'woima-common-defaults' => {
- 'static' => setv(T_BOOL, 0, 1, 1, 0, undef),
- 'wildcard' => setv(T_BOOL, 0, 1, 1, 0, undef),
- 'mx' => setv(T_OFQDN, 0, 1, 1, '', undef),
- 'backupmx' => setv(T_BOOL, 0, 1, 1, 0, undef),
- 'custom' => setv(T_BOOL, 0, 1, 1, 0, undef),
- 'script' => setv(T_STRING, 1, 1, 1, '/nic/update', undef),
- },
- 'woima-service-common-defaults' => {
- 'server' => setv(T_FQDNP, 1, 0, 1, 'dyn.woima.fi', undef),
- 'login' => setv(T_LOGIN, 1, 0, 1, '', undef),
- 'password' => setv(T_PASSWD, 1, 0, 1, '', undef),
- 'ip' => setv(T_IP, 0, 1, 0, undef, undef),
- 'wtime' => setv(T_DELAY, 0, 1, 1, 0, interval('30s')),
- 'mtime' => setv(T_NUMBER, 0, 1, 0, 0, undef),
- 'atime' => setv(T_NUMBER, 0, 1, 0, 0, undef),
- 'status' => setv(T_ANY, 0, 1, 0, '', undef),
- 'min-interval' => setv(T_DELAY, 0, 0, 1, interval('30s'), 0),
- 'max-interval' => setv(T_DELAY, 0, 0, 1, interval('25d'), 0),
- 'min-error-interval' => setv(T_DELAY, 0, 0, 1, interval('5m'), 0),
- 'warned-min-interval' => setv(T_ANY, 0, 1, 0, 0, undef),
- 'warned-min-error-interval' => setv(T_ANY, 0, 1, 0, 0, undef),
- },
- 'yandex-common-defaults' => {
- 'server' => setv(T_FQDNP, 1, 0, 1, 'pddimp.yandex.ru', undef),
- },
- 'dnsmadeeasy-common-defaults' => {
- 'server' => setv(T_FQDNP, 1, 0, 1, 'cp.dnsmadeeasy.com', undef),
- 'script' => setv(T_STRING, 1, 1, 1, '/servlet/updateip', undef),
- },
- 'dondominio-common-defaults' => {
- 'server' => setv(T_FQDNP, 1, 0, 1, 'dondns.dondominio.com', undef),
- },
-);
-my %services = (
- 'dyndns1' => {
- 'updateable' => \&nic_dyndns2_updateable,
- 'update' => \&nic_dyndns1_update,
- 'examples' => \&nic_dyndns1_examples,
- 'variables' => merge(
- $variables{'dyndns-common-defaults'},
- $variables{'service-common-defaults'},
- ),
- },
- 'dyndns2' => {
- 'updateable' => \&nic_dyndns2_updateable,
- 'update' => \&nic_dyndns2_update,
- 'examples' => \&nic_dyndns2_examples,
- 'variables' => merge(
- { 'custom' => setv(T_BOOL, 0, 1, 1, 0, undef), },
- { 'script' => setv(T_STRING, 1, 1, 1, '/nic/update', undef), },
-# { 'offline' => setv(T_BOOL, 0, 1, 1, 0, undef), },
- $variables{'dyndns-common-defaults'},
- $variables{'service-common-defaults'},
- ),
- },
- 'noip' => {
- 'updateable' => undef,
- 'update' => \&nic_noip_update,
- 'examples' => \&nic_noip_examples,
- 'variables' => merge(
- { 'custom' => setv(T_BOOL, 0, 1, 1, 0, undef), },
- $variables{'noip-common-defaults'},
- $variables{'noip-service-common-defaults'},
- ),
- },
- 'concont' => {
- 'updateable' => undef,
- 'update' => \&nic_concont_update,
- 'examples' => \&nic_concont_examples,
- 'variables' => merge(
- $variables{'service-common-defaults'},
- { 'mx' => setv(T_OFQDN, 0, 1, 1, '', undef), },
- { 'wildcard' => setv(T_BOOL, 0, 1, 1, 0, undef), },
- ),
- },
- 'dslreports1' => {
- 'updateable' => undef,
- 'update' => \&nic_dslreports1_update,
- 'examples' => \&nic_dslreports1_examples,
- 'variables' => merge(
- { 'host' => setv(T_NUMBER, 1, 1, 1, 0, undef) },
- $variables{'service-common-defaults'},
- ),
- },
- 'hammernode1' => {
- 'updateable' => undef,
- 'update' => \&nic_hammernode1_update,
- 'examples' => \&nic_hammernode1_examples,
- 'variables' => merge(
- { 'server' => setv(T_FQDNP, 1, 0, 1, 'dup.hn.org', undef) },
- { 'min-interval' => setv(T_DELAY, 0, 0, 1, interval('5m'), 0),},
- $variables{'service-common-defaults'},
- ),
- },
- 'zoneedit1' => {
- 'updateable' => undef,
- 'update' => \&nic_zoneedit1_update,
- 'examples' => \&nic_zoneedit1_examples,
- 'variables' => merge(
- { 'server' => setv(T_FQDNP, 1, 0, 1, 'dynamic.zoneedit.com', undef) },
- { 'min-interval' => setv(T_DELAY, 0, 0, 1, interval('5m'), 0),},
- $variables{'service-common-defaults'},
- $variables{'zoneedit-service-common-defaults'},
- ),
- },
- 'easydns' => {
- 'updateable' => undef,
- 'update' => \&nic_easydns_update,
- 'examples' => \&nic_easydns_examples,
- 'variables' => merge(
- { 'server' => setv(T_FQDNP, 1, 0, 1, 'members.easydns.com', undef) },
- { 'min-interval' => setv(T_DELAY, 0, 0, 1, interval('5m'), 0),},
- $variables{'easydns-common-defaults'},
- $variables{'service-common-defaults'},
- ),
- },
- 'dnspark' => {
- 'updateable' => undef,
- 'update' => \&nic_dnspark_update,
- 'examples' => \&nic_dnspark_examples,
- 'variables' => merge(
- { 'server' => setv(T_FQDNP, 1, 0, 1, 'www.dnspark.com', undef) },
- { 'min-interval' => setv(T_DELAY, 0, 0, 1, interval('5m'), 0),},
- $variables{'dnspark-common-defaults'},
- $variables{'service-common-defaults'},
- ),
- },
- 'namecheap' => {
- 'updateable' => undef,
- 'update' => \&nic_namecheap_update,
- 'examples' => \&nic_namecheap_examples,
- 'variables' => merge(
- { 'server' => setv(T_FQDNP, 1, 0, 1, 'dynamicdns.park-your-domain.com', undef) },
- { 'min-interval' => setv(T_DELAY, 0, 0, 1, 0, interval('5m')),},
- $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' => {
- 'updateable' => undef,
- 'update' => \&nic_sitelutions_update,
- 'examples' => \&nic_sitelutions_examples,
- 'variables' => merge(
- { 'server' => setv(T_FQDNP, 1, 0, 1, 'www.sitelutions.com', undef) },
- { 'min-interval' => setv(T_DELAY, 0, 0, 1, 0, interval('5m')),},
- $variables{'service-common-defaults'},
- ),
- },
- 'freedns' => {
- 'updateable' => undef,
- 'update' => \&nic_freedns_update,
- 'examples' => \&nic_freedns_examples,
- 'variables' => merge(
- { 'server' => setv(T_FQDNP, 1, 0, 1, 'freedns.afraid.org', undef) },
- { 'min-interval' => setv(T_DELAY, 0, 0, 1, 0, interval('5m')),},
- $variables{'service-common-defaults'},
- ),
- },
- 'changeip' => {
- 'updateable' => undef,
- 'update' => \&nic_changeip_update,
- 'examples' => \&nic_changeip_examples,
- 'variables' => merge(
- { 'server' => setv(T_FQDNP, 1, 0, 1, 'nic.changeip.com', undef) },
- { 'min-interval' => setv(T_DELAY, 0, 0, 1, 0, interval('5m')),},
- $variables{'service-common-defaults'},
- ),
- },
- 'dtdns' => {
- 'updateable' => undef,
- 'update' => \&nic_dtdns_update,
- 'examples' => \&nic_dtdns_examples,
- 'variables' => merge(
- $variables{'dtdns-common-defaults'},
- $variables{'service-common-defaults'},
- ),
- },
- 'nsupdate' => {
- 'updateable' => undef,
- 'update' => \&nic_nsupdate_update,
- 'examples' => \&nic_nsupdate_examples,
- 'variables' => merge(
- { 'login' => setv(T_LOGIN, 1, 0, 1, '/usr/bin/nsupdate', undef), },
- $variables{'nsupdate-common-defaults'},
- $variables{'service-common-defaults'},
- ),
- },
- 'cloudflare' => {
- 'updateable' => undef,
- 'update' => \&nic_cloudflare_update,
- 'examples' => \&nic_cloudflare_examples,
- 'variables' => merge(
- { 'server' => setv(T_FQDNP, 1, 0, 1, 'api.cloudflare.com/client/v4', undef) },
- { 'min-interval' => setv(T_DELAY, 0, 0, 1, interval('5m'), 0),},
- $variables{'cloudflare-common-defaults'},
- $variables{'service-common-defaults'},
- ),
- },
- 'googledomains' => {
- 'updateable' => undef,
- 'update' => \&nic_googledomains_update,
- 'examples' => \&nic_googledomains_examples,
- 'variables' => merge(
- { 'min-interval' => setv(T_DELAY, 0, 0, 1, interval('5m'), 0),},
- $variables{'googledomains-common-defaults'},
- $variables{'service-common-defaults'},
- ),
- },
- 'duckdns' => {
- 'updateable' => undef,
- 'update' => \&nic_duckdns_update,
- 'examples' => \&nic_duckdns_examples,
- 'variables' => merge(
- $variables{'duckdns-common-defaults'},
- $variables{'service-common-defaults'},
- ),
- },
- 'freemyip' => {
- 'updateable' => undef,
- 'update' => \&nic_freemyip_update,
- 'examples' => \&nic_freemyip_examples,
- 'variables' => merge(
- $variables{'freemyip-common-defaults'},
- $variables{'service-common-defaults'},
- ),
- },
- 'woima' => {
- 'updateable' => undef,
- 'update' => \&nic_woima_update,
- 'examples' => \&nic_woima_examples,
- 'variables' => merge(
- $variables{'woima-common-defaults'},
- $variables{'woima-service-common-defaults'},
- ),
- },
- 'yandex' => {
- 'updateable' => undef,
- 'update' => \&nic_yandex_update,
- 'examples' => \&nic_yandex_examples,
- 'variables' => merge(
- { 'min-interval' => setv(T_DELAY, 0, 0, 1, interval('5m'), 0),},
- $variables{'yandex-common-defaults'},
- $variables{'service-common-defaults'},
- ),
- },
- 'dnsmadeeasy' => {
- 'updateable' => undef,
- 'update' => \&nic_dnsmadeeasy_update,
- 'examples' => \&nic_dnsmadeeasy_examples,
- 'variables' => merge(
- $variables{'dnsmadeeasy-common-defaults'},
- $variables{'service-common-defaults'},
- ),
- },
- 'dondominio' => {
- 'updateable' => undef,
- 'update' => \&nic_dondominio_update,
- 'examples' => \&nic_dondominio_examples,
- 'variables' => merge(
- $variables{'dondominio-common-defaults'},
- $variables{'service-common-defaults'},
- ),
- },
-);
-$variables{'merged'} = merge($variables{'global-defaults'},
- $variables{'service-common-defaults'},
- $variables{'dyndns-common-defaults'},
- map { $services{$_}{'variables'} } keys %services,
-);
-
-my @opt = (
- "usage: ${program} [options]",
- "options are:",
- [ "daemon", "=s", "-daemon delay : run as a daemon, specify delay as an interval." ],
- [ "foreground", "!", "-foreground : do not fork" ],
- [ "proxy", "=s", "-proxy host : use 'host' as the HTTP proxy" ],
- [ "server", "=s", "-server host : update DNS information on 'host'" ],
- [ "protocol", "=s", "-protocol type : update protocol used" ],
- [ "file", "=s", "-file path : load configuration information from 'path'" ],
- [ "cache", "=s", "-cache path : record address used in 'path'" ],
- [ "pid", "=s", "-pid path : record process id in 'path'" ],
- "",
- [ "use", "=s", "-use which : how the should IP address be obtained." ],
- &ip_strategies_usage(),
- "",
- [ "ip", "=s", "-ip address : set the IP address to 'address'" ],
- "",
- [ "if", "=s", "-if interface : obtain IP address from 'interface'" ],
- [ "if-skip", "=s", "-if-skip pattern : skip any IP addresses before 'pattern' in the output of ifconfig {if}" ],
- "",
- [ "web", "=s", "-web provider|url : obtain IP address from provider's IP checking page" ],
- [ "web-skip", "=s", "-web-skip pattern : skip any IP addresses before 'pattern' on the web provider|url" ],
- "",
- [ "fw", "=s", "-fw address|url : obtain IP address from firewall at 'address'" ],
- [ "fw-skip", "=s", "-fw-skip pattern : skip any IP addresses before 'pattern' on the firewall address|url" ],
- [ "fw-banlocal", "!", "-fw-banlocal : ignore local IP addresses on the firewall address|url" ],
- [ "fw-login", "=s", "-fw-login login : use 'login' when getting IP from fw" ],
- [ "fw-password", "=s", "-fw-password secret : use password 'secret' when getting IP from fw" ],
- "",
- [ "cmd", "=s", "-cmd program : obtain IP address from by calling {program}" ],
- [ "cmd-skip", "=s", "-cmd-skip pattern : skip any IP addresses before 'pattern' in the output of {cmd}" ],
- "",
- [ "login", "=s", "-login user : login as 'user'" ],
- [ "password", "=s", "-password secret : use password 'secret'" ],
- [ "host", "=s", "-host host : update DNS information for 'host'" ],
- "",
- [ "options", "=s", "-options opt,opt : optional per-service arguments (see below)" ],
- "",
- [ "ssl", "!", "-{no}ssl : do updates over encrypted SSL connection" ],
- [ "retry", "!", "-{no}retry : retry failed updates." ],
- [ "force", "!", "-{no}force : force an update even if the update may be unnecessary" ],
- [ "timeout", "=i", "-timeout max : wait at most 'max' seconds for the host to respond" ],
-
- [ "syslog", "!", "-{no}syslog : log messages to syslog" ],
- [ "facility", "=s", "-facility {type} : log messages to syslog to facility {type}" ],
- [ "priority", "=s", "-priority {pri} : log messages to syslog with priority {pri}" ],
- [ "mail", "=s", "-mail address : e-mail messages to {address}" ],
- [ "mail-failure","=s", "-mail-failure address : e-mail messages for failed updates to {address}" ],
- [ "exec", "!", "-{no}exec : do {not} execute; just show what would be done" ],
- [ "debug", "!", "-{no}debug : print {no} debugging information" ],
- [ "verbose", "!", "-{no}verbose : print {no} verbose information" ],
- [ "quiet", "!", "-{no}quiet : print {no} messages for unnecessary updates" ],
- [ "ipv6", "!", "-{no}ipv6 : use ipv6" ],
- [ "help", "", "-help : this message" ],
- [ "postscript", "", "-postscript : script to run after updating ddclient, has new IP as param" ],
-
- [ "query", "!", "-{no}query : print {no} ip addresses and exit" ],
- [ "test", "!", "" ], ## hidden
- [ "geturl", "=s", "" ], ## hidden
- "",
- nic_examples(),
- "$program version $version, ",
- " originally written by Paul Burry, paul+ddclient\@burry.ca",
- " project now maintained on http://ddclient.sourceforge.net"
-);
-
-## process args
-my ($opt_usage, %opt) = process_args(@opt);
-my ($result, %config, %globals, %cache);
-my $saved_cache = '';
-my %saved_opt = %opt;
-$result = 'OK';
-
-test_geturl(opt('geturl')) if opt('geturl');
-
-## process help option
-if (opt('help')) {
- *STDERR = *STDOUT;
- usage(0);
-}
-
-## read config file because 'daemon' mode may be defined there.
-read_config(define($opt{'file'}, default('file')), \%config, \%globals);
-init_config();
-test_possible_ip() if opt('query');
-
-if (!opt('daemon') && $programd =~ /d$/) {
- $opt{'daemon'} = minimum('daemon');
-}
-my $caught_hup = 0;
-my $caught_term = 0;
-my $caught_kill = 0;
-$SIG{'HUP'} = sub { $caught_hup = 1; };
-$SIG{'TERM'} = sub { $caught_term = 1; };
-$SIG{'KILL'} = sub { $caught_kill = 1; };
-# don't fork() if foreground or force is on
-if (opt('foreground') || opt('force')) {
- ;
-} elsif (opt('daemon')) {
- $SIG{'CHLD'} = 'IGNORE';
- my $pid = fork;
- if ($pid < 0) {
- print STDERR "${program}: can not fork ($!)\n";
- exit -1;
- } elsif ($pid) {
- exit 0;
- }
- $SIG{'CHLD'} = 'DEFAULT';
- open(STDOUT, ">/dev/null");
- open(STDERR, ">/dev/null");
- open(STDIN, " 0) && !$caught_hup && !$caught_term && !$caught_kill) {
- my $delay = $left > 10 ? 10 : $left;
-
- $0 = sprintf("%s - sleeping for %s seconds", $program, $left);
- $left -= sleep $delay;
- # preventing deep sleep - see [bugs:#46]
- if ($left > $daemon) {
- $left = $daemon;
- }
- }
- $caught_hup = 0;
- $result = 0;
-
- } elsif (! scalar(%config)) {
- warning("no hosts to update.") unless !opt('quiet') || opt('verbose') || !$daemon;
- $result = 1;
-
- } else {
- $result = $result eq 'OK' ? 0 : 1;
- }
-} while ($daemon && !$result && !$caught_term && !$caught_kill);
-
-warning("caught SIGKILL; exiting") if $caught_kill;
-unlink_pid();
-sendmail();
-
-exit($result);
-
-######################################################################
-## runpostscript
-######################################################################
-
-sub runpostscript {
- my ($ip) = @_;
-
- if ( defined $globals{postscript} ) {
- if ( -x $globals{postscript}) {
- system ("$globals{postscript} $ip &");
- } else {
- warning ("Can not execute post script: %s", $globals{postscript});
- }
- }
-}
-
-######################################################################
-## update_nics
-######################################################################
-sub update_nics {
- my %examined = ();
- my %iplist = ();
-
- foreach my $s (sort keys %services) {
- my (@hosts, %ips) = ();
- my $updateable = $services{$s}{'updateable'};
- my $update = $services{$s}{'update'};
-
- foreach my $h (sort keys %config) {
- next if $config{$h}{'protocol'} ne lc($s);
- $examined{$h} = 1;
- # we only do this once per 'use' and argument combination
- my $use = opt('use', $h);
- my $arg_ip = opt('ip', $h) || '';
- my $arg_fw = opt('fw', $h) || '';
- my $arg_if = opt('if', $h) || '';
- my $arg_web = opt('web', $h) || '';
- my $arg_cmd = opt('cmd', $h) || '';
- my $ip = "";
- if (exists $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd}) {
- $ip = $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd};
- } else {
- $ip = get_ip($use, $h);
- if (!defined $ip || !$ip) {
- warning("unable to determine IP address")
- if !$daemon || opt('verbose');
- next;
- }
- if ($ip !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
- if( !ipv6_match($ip) ) {
- warning("malformed IP address (%s)", $ip);
- next;
- }
- }
- $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd} = $ip;
- }
- $config{$h}{'wantip'} = $ip;
- next if !nic_updateable($h, $updateable);
- push @hosts, $h;
- $ips{$ip} = $h;
- }
- if (@hosts) {
- $0 = sprintf("%s - updating %s", $program, join(',', @hosts));
- &$update(@hosts);
- runpostscript(join ' ', keys %ips);
- }
- }
- foreach my $h (sort keys %config) {
- if (!exists $examined{$h}) {
- failed("%s was not updated because protocol %s is not supported.",
- $h, define($config{$h}{'protocol'}, '')
- );
- }
- }
- write_cache(opt('cache'));
-}
-######################################################################
-## unlink_pid()
-######################################################################
-sub unlink_pid {
- if (opt('pid') && opt('daemon')) {
- unlink opt('pid');
- }
-}
-
-######################################################################
-## write_pid()
-######################################################################
-sub write_pid {
- my $file = opt('pid');
-
- if ($file && opt('daemon')) {
- local *FD;
- if (! open(FD, "> $file")) {
- warning("Cannot create file '%s'. ($!)", $file);
-
- } else {
- printf FD "$$\n";
- close(FD);
- }
- }
-}
-
-######################################################################
-## write_cache($file)
-######################################################################
-sub write_cache {
- my ($file) = @_;
-
- ## merge the updated host entries into the cache.
- foreach my $h (keys %config) {
- if (! exists $cache{$h} || $config{$h}{'update'}) {
- map {$cache{$h}{$_} = $config{$h}{$_} } @{$config{$h}{'cacheable'}};
-
- } else {
- map {$cache{$h}{$_} = $config{$h}{$_} } qw(atime wtime status);
- }
- }
-
- ## construct the cache file.
- my $cache = "";
- foreach my $h (sort keys %cache) {
- my $opt = join(',', map { "$_=".define($cache{$h}{$_},'') } sort keys %{$cache{$h}});
-
- $cache .= sprintf "%s%s%s\n", $opt, ($opt ? ' ' : ''), $h;
- }
- $file = '' if defined($saved_cache) && $cache eq $saved_cache;
-
- ## write the updates and other entries to the cache file.
- if ($file) {
- $saved_cache = undef;
- local *FD;
- if (! open(FD, "> $file")) {
- fatal("Cannot create file '%s'. ($!)", $file);
- }
- printf FD "## $program-$version\n";
- printf FD "## last updated at %s (%d)\n", prettytime($now), $now;
- printf FD $cache;
-
- close(FD);
- }
-}
-######################################################################
-## read_cache($file) - called before reading the .conf
-######################################################################
-sub read_cache {
- my $file = shift;
- my $config = shift;
- my $globals = {};
-
- %{$config} = ();
- ## read the cache file ignoring anything on the command-line.
- if (-e $file) {
- my %saved = %opt;
- %opt = ();
- $saved_cache = _read_config($config, $globals, "##\\s*$program-$version\\s*", $file);
- %opt = %saved;
-
- foreach my $h (keys %cache) {
- if (exists $config->{$h}) {
- foreach (qw(atime mtime wtime ip status)) {
- $config->{$h}{$_} = $cache{$h}{$_} if exists $cache{$h}{$_};
- }
- }
- }
- }
-}
-######################################################################
-## parse_assignments(string) return (rest, %variables)
-## parse_assignment(string) return (name, value, rest)
-######################################################################
-sub parse_assignments {
- my $rest = shift;
- my @args = @_;
- my %variables = ();
- my ($name, $value);
-
- while (1) {
- $rest =~ s/^\s+//;
- ($name, $value, $rest) = parse_assignment($rest, @args);
- if (defined $name) {
- $variables{$name} = $value;
- } else {
- last;
- }
- }
- return ($rest, %variables);
-}
-sub parse_assignment {
- my $rest = shift;
- my $stop = @_ ? shift : '[\n\s,]';
- my ($c, $name, $value);
- my ($escape, $quote) = (0, '');
-
- if ($rest =~ /^\s*([a-z][0-9a-z_-]*)=(.*)/i) {
- ($name, $rest, $value) = ($1, $2, '');
-
- while (length($c = substr($rest,0,1))) {
- $rest = substr($rest,1);
- if ($escape) {
- $value .= $c;
- $escape = 0;
- } elsif ($c eq "\\") {
- $escape = 1;
- } elsif ($quote && $c eq $quote) {
- $quote = ''
- } elsif (!$quote && $c =~ /[\'\"]/) {
- $quote = $c;
- } elsif (!$quote && $c =~ /^${stop}/) {
- last;
- } else {
- $value .= $c;
- }
- }
- }
- warning("assignment ended with an open quote") if $quote;
- return ($name, $value, $rest);
-}
-######################################################################
-## read_config
-######################################################################
-sub read_config {
- my $file = shift;
- my $config = shift;
- my $globals = shift;
- my %globals = ();
-
- _read_config($config, $globals, '', $file, %globals);
-}
-sub _read_config {
- my $config = shift;
- my $globals = shift;
- my $stamp = shift;
- local $file = shift;
- my %globals = @_;
- my %config = ();
- my $content = '';
-
- local *FD;
- if (! open(FD, "< $file")) {
- # fatal("Cannot open file '%s'. ($!)", $file);
- warning("Cannot open file '%s'. ($!)", $file);
- }
- # Check for only owner has any access to config file
- my ($dev, $ino, $mode, @statrest) = stat(FD);
- if ($mode & 077) {
- if (-f FD && (chmod 0600, $file)) {
- warning("file $file must be accessible only by its owner (fixed).");
- } else {
- # fatal("file $file must be accessible only by its owner.");
- warning("file $file must be accessible only by its owner.");
- }
- }
-
- local $lineno = 0;
- my $continuation = '';
- my %passwords = ();
- while () {
- s/[\r\n]//g;
-
- $lineno++;
-
- ## check for the program version stamp
- if (($. == 1) && $stamp && ($_ !~ /^$stamp$/i)) {
- warning("program version mismatch; ignoring %s", $file);
- last;
- }
- if (/\\\s+$/) {
- warning("whitespace follows the \\ at the end-of-line.\nIf you meant to have a line continuation, remove the trailing whitespace.");
- }
-
- $content .= "$_\n" unless /^#/;
-
- ## parsing passwords is special
- if (/^([^#]*\s)?([^#]*?password\S*?)\s*=\s*('.*'|[^']\S*)(.*)/) {
- my ($head, $key, $value, $tail) = ($1 || '', $2, $3, $4);
- $value = $1 if $value =~ /^'(.*)'$/;
- $passwords{$key} = $value;
- $_ = "${head}${key}=dummy${tail}";
- }
-
- ## remove comments
- s/#.*//;
-
- ## handle continuation lines
- $_ = "$continuation$_";
- if (/\\$/) {
- chop;
- $continuation = $_;
- next;
- }
- $continuation = '';
-
- s/^\s+//; # remove leading white space
- s/\s+$//; # remove trailing white space
- s/\s+/ /g; # canonify
- next if /^$/;
-
- ## expected configuration line is:
- ## [opt=value,opt=..] [host [login [password]]]
- my %locals;
- ($_, %locals) = parse_assignments($_);
- s/\s*,\s*/,/g;
- my @args = split;
-
- ## verify that keywords are valid...and check the value
- foreach my $k (keys %locals) {
- $locals{$k} = $passwords{$k} if defined $passwords{$k};
- if (!exists $variables{'merged'}{$k}) {
- warning("unrecognized keyword '%s' (ignored)", $k);
- delete $locals{$k};
- } else {
- 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});
- delete $locals{$k};
- } else { $locals{$k} = $value; }
- }
- }
- if (exists($locals{'host'})) {
- $args[0] = @args ? "$args[0],$locals{host}" : "$locals{host}";
- }
- ## accumulate globals
- if ($#args < 0) {
- map { $globals{$_} = $locals{$_} } keys %locals;
- }
-
- ## process this host definition
- if (@args) {
- my ($host, $login, $password) = @args;
-
- ## add in any globals..
- %locals = %{ merge(\%locals, \%globals) };
-
- ## override login and password if specified the old way.
- $locals{'login'} = $login if defined $login;
- $locals{'password'} = $password if defined $password;
-
- ## allow {host} to be a comma separated list of hosts
- foreach my $h (split_by_comma($host)) {
- ## save a copy of the current globals
- $config{$h} = { %locals };
- $config{$h}{'host'} = $h;
- }
- }
- %passwords = ();
- }
- close(FD);
-
- warning("file ends while expecting a continuation line.")
- if $continuation;
-
- %$globals = %globals;
- %$config = %config;
-
- return $content;
-}
-######################################################################
-## init_config -
-######################################################################
-sub init_config {
- %opt = %saved_opt;
-
- ##
- $opt{'quiet'} = 0 if opt('verbose');
-
- ## infer the IP strategy if possible
- $opt{'use'} = 'ip' if !define($opt{'use'}) && defined($opt{'ip'});
- $opt{'use'} = 'if' if !define($opt{'use'}) && defined($opt{'if'});
- $opt{'use'} = 'web' if !define($opt{'use'}) && defined($opt{'web'});
-
- ## 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;
-
- ## only set $opt{'daemon'} if it has been explicitly passed in
- if (define($opt{'daemon'},$globals{'daemon'},0)) {
- $opt{'daemon'} = interval(opt('daemon'));
- $opt{'daemon'} = minimum('daemon')
- if ($opt{'daemon'} < minimum('daemon'));
- }
-
- ## define or modify host options specified on the command-line
- if (exists $opt{'options'} && defined $opt{'options'}) {
- ## collect cmdline configuration options.
- my %options = ();
- foreach my $opt (split_by_comma($opt{'options'})) {
- my ($name,$var) = split /\s*=\s*/, $opt;
- $options{$name} = $var;
- }
- ## determine hosts specified with -host
- my @hosts = ();
- if (exists $opt{'host'}) {
- foreach my $h (split_by_comma($opt{'host'})) {
- push @hosts, $h;
- }
- }
- ## and those in -options=...
- if (exists $options{'host'}) {
- foreach my $h (split_by_comma($options{'host'})) {
- push @hosts, $h;
- }
- delete $options{'host'};
- }
- ## merge options into host definitions or globals
- if (@hosts) {
- foreach my $h (@hosts) {
- $config{$h} = merge(\%options, $config{$h});
- }
- $opt{'host'} = join(',', @hosts);
- } else {
- %globals = %{ merge(\%options, \%globals) };
- }
- }
-
- ## override global options with those on the command-line.
- foreach my $o (keys %opt) {
- if (defined $opt{$o} && exists $variables{'global-defaults'}{$o}) {
- $globals{$o} = $opt{$o};
- }
- }
-
- ## sanity check
- if (defined $opt{'host'} && defined $opt{'retry'}) {
- usage("options -retry and -host (or -option host=..) are mutually exclusive");
- }
-
- ## determine hosts to update (those on the cmd-line, config-file, or failed cached)
- my @hosts = keys %config;
- if (opt('host')) {
- @hosts = split_by_comma($opt{'host'});
- }
- if (opt('retry')) {
- @hosts = map { $_ if $cache{$_}{'status'} ne 'good' } keys %cache;
- }
-
- ## remove any other hosts
- my %hosts;
- map { $hosts{$_} = undef } @hosts;
- map { delete $config{$_} unless exists $hosts{$_} } keys %config;
-
- ## collect the cacheable variables.
- foreach my $proto (keys %services) {
- my @cacheable = ();
- foreach my $k (keys %{$services{$proto}{'variables'}}) {
- push @cacheable, $k if $services{$proto}{'variables'}{$k}{'cache'};
- }
- $services{$proto}{'cacheable'} = [ @cacheable ];
- }
-
- ## sanity check..
- ## make sure config entries have all defaults and they meet minimums
- ## first the globals...
- foreach my $k (keys %globals) {
- my $def = $variables{'merged'}{$k};
- my $ovalue = define($globals{$k}, $def->{'default'});
- my $value = check_value($ovalue, $def);
- if ($def->{'required'} && !defined $value) {
- $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:
- foreach my $h (keys %config) {
- my $proto;
- $proto = $config{$h}{'protocol'};
- $proto = opt('protocol') if !defined($proto);
-
- load_sha1_support($proto) if (grep (/^$proto$/, ("freedns", "nfsn")));
- load_json_support($proto) if (grep (/^$proto$/, ("cloudflare","yandex","nfsn")));
-
- if (!exists($services{$proto})) {
- warning("skipping host: %s: unrecognized protocol '%s'", $h, $proto);
- delete $config{$h};
-
- } else {
- my $svars = $services{$proto}{'variables'};
- my $conf = { 'protocol' => $proto };
-
- foreach my $k (keys %$svars) {
- my $def = $svars->{$k};
- my $ovalue = define($config{$h}{$k}, $def->{'default'});
- my $value = check_value($ovalue, $def);
- if ($def->{'required'} && !defined $value) {
- warning("skipping host: %s: '%s=%s' is an invalid %s.", $h, $k, $ovalue, $def->{'type'});
- delete $config{$h};
- next HOST;
- }
- $conf->{$k} = $value;
-
- }
- $config{$h} = $conf;
- $config{$h}{'cacheable'} = [ @{$services{$proto}{'cacheable'}} ];
- }
- }
-}
-
-######################################################################
-## usage
-######################################################################
-sub usage {
- my $exitcode = 1;
- $exitcode = shift if @_ != 0; # use first arg if given
- my $msg = '';
- if (@_) {
- my $format = shift;
- $msg .= sprintf $format, @_;
- 1 while chomp($msg);
- $msg .= "\n";
- }
- printf STDERR "%s%s\n", $msg, $opt_usage;
- sendmail();
- exit $exitcode;
-}
-
-######################################################################
-## process_args -
-######################################################################
-sub process_args {
- my @spec = ();
- my $usage = "";
- my %opts = ();
-
- foreach (@_) {
- if (ref $_) {
- my ($key, $specifier, $arg_usage) = @$_;
- my $value = default($key);
-
- ## add a option specifier
- push @spec, $key . $specifier;
-
- ## define the default value which can be overwritten later
- $opt{$key} = undef;
-
- next unless $arg_usage;
-
- ## add a line to the usage;
- $usage .= " $arg_usage";
- if (defined($value) && $value ne '') {
- $usage .= " (default: ";
- if ($specifier eq '!') {
- $usage .= "no" if ($specifier eq '!') && !$value;
- $usage .= $key;
- } else {
- $usage .= $value;
- }
- $usage .= ")";
- }
- $usage .= ".";
- } else {
- $usage .= $_;
- }
- $usage .= "\n";
- }
- ## process the arguments
- if (! GetOptions(\%opt, @spec)) {
- $opt{"help"} = 1;
- }
- return ($usage, %opt);
-}
-######################################################################
-## test_possible_ip - print possible IPs
-######################################################################
-sub test_possible_ip {
- local $opt{'debug'} = 0;
-
- printf "use=ip, ip=%s address is %s\n", opt('ip'), define(get_ip('ip'), 'NOT FOUND')
- if defined opt('ip');
-
- {
- local $opt{'use'} = 'if';
- foreach my $if (grep {/^[a-zA-Z]/} `ifconfig -a`) {
- $if =~ s/:?\s.*//is;
- local $opt{'if'} = $if;
- printf "use=if, if=%s address is %s\n", opt('if'), define(get_ip('if'), 'NOT FOUND');
- }
- }
- if (opt('fw')) {
- if (opt('fw') !~ m%/%) {
- foreach my $fw (sort keys %builtinfw) {
- local $opt{'use'} = $fw;
- printf "use=$fw address is %s\n", define(get_ip($fw), 'NOT FOUND');
- }
- }
- local $opt{'use'} = 'fw';
- printf "use=fw, fw=%s address is %s\n", opt('fw'), define(get_ip(opt('fw')), 'NOT FOUND')
- if ! exists $builtinfw{opt('fw')};
-
- }
- {
- local $opt{'use'} = 'web';
- foreach my $web (sort keys %builtinweb) {
- local $opt{'web'} = $web;
- printf "use=web, web=$web address is %s\n", define(get_ip('web'), 'NOT FOUND');
- }
- printf "use=web, web=%s address is %s\n", opt('web'), define(get_ip('web'), 'NOT FOUND')
- if ! exists $builtinweb{opt('web')};
- }
- if (opt('cmd')) {
- local $opt{'use'} = 'cmd';
- printf "use=cmd, cmd=%s address is %s\n", opt('cmd'), define(get_ip('cmd'), 'NOT FOUND');
- }
- exit 0 unless opt('debug');
-}
-######################################################################
-## test_geturl - print (and save if -test) result of fetching a URL
-######################################################################
-sub test_geturl {
- my $url = shift;
-
- my $reply = geturl(opt('proxy'), $url, opt('login'), opt('password'));
- print "URL $url\n";;
- print defined($reply) ? $reply : "\n";
- exit;
-}
-######################################################################
-## load_file
-######################################################################
-sub load_file {
- my $file = shift;
- my $buffer = '';
-
- if (exists($ENV{'TEST_CASE'})) {
- my $try = "$file-$ENV{'TEST_CASE'}";
- $file = $try if -f $try;
- }
-
- local *FD;
- if (open(FD, "< $file")) {
- read(FD, $buffer, -s FD);
- close(FD);
- debug("Loaded %d bytes from %s", length($buffer), $file);
- } else {
- debug("Load failed from %s ($!)", $file);
- }
- return $buffer
-}
-######################################################################
-## save_file
-######################################################################
-sub save_file {
- my ($file, $buffer, $opt) = @_;
-
- $file .= "-$ENV{'TEST_CASE'}" if exists $ENV{'TEST_CASE'};
- if (defined $opt) {
- my $i = 0;
- while (-f "$file-$i") {
- if ('unique' =~ /^$opt/i) {
- my $a = join('\n', grep {!/^Date:/} split /\n/, $buffer);
- my $b = join('\n', grep {!/^Date:/} split /\n/, load_file("$file-$i"));
- last if $a eq $b;
- }
- $i++;
- }
- $file = "$file-$i";
- }
- debug("Saving to %s", $file);
- local *FD;
- open(FD, "> $file") or return;
- print FD $buffer;
- close(FD);
- return $buffer;
-}
-######################################################################
-## print_opt
-## print_globals
-## print_config
-## print_cache
-## print_info
-######################################################################
-sub _print_hash {
- my ($string, $ptr) = @_;
- my $value = $ptr;
-
- if (! defined($ptr)) {
- $value = "";
- } elsif (ref $ptr eq 'HASH') {
- foreach my $key (sort keys %$ptr) {
- _print_hash("${string}\{$key\}", $ptr->{$key});
- }
- return;
- }
- printf "%-36s : %s\n", $string, $value;
-}
-sub print_hash {
- my ($string, $hash) = @_;
- printf "=== %s ====\n", $string;
- _print_hash($string, $hash);
-}
-sub print_opt { print_hash("opt", \%opt); }
-sub print_globals { print_hash("globals", \%globals); }
-sub print_config { print_hash("config", \%config); }
-sub print_cache { print_hash("cache", \%cache); }
-sub print_info {
- print_opt();
- print_globals();
- print_config();
- print_cache();
-}
-######################################################################
-## pipecmd - run an external command
-## logger
-## sendmail
-######################################################################
-sub pipecmd {
- my $cmd = shift;
- my $stdin = join("\n", @_);
- my $ok = 0;
-
- ## remove trailing newlines
- 1 while chomp($stdin);
-
- ## override when debugging.
- $cmd = opt('exec') ? "| $cmd" : "> /dev/null";
-
- ## execute the command.
- local *FD;
- if (! open(FD, $cmd)) {
- printf STDERR "$program: cannot execute command %s.\n", $cmd;
-
- } elsif ($stdin && (! print FD "$stdin\n")) {
- printf STDERR "$program: failed writting to %s.\n", $cmd;
- close(FD);
-
- } elsif (! close(FD)) {
- printf STDERR "$program: failed closing %s.($@)\n", $cmd;
-
- } elsif (opt('exec') && $?) {
- printf STDERR "$program: failed %s. ($@)\n", $cmd;
-
- } else {
- $ok = 1;
- }
- return $ok;
-}
-sub logger {
- if (opt('syslog') && opt('facility') && opt('priority')) {
- my $facility = opt('facility');
- my $priority = opt('priority');
- return pipecmd("logger -p$facility.$priority -t${program}\[$$\]", @_);
- }
- return 1;
-}
-sub sendmail {
- my $recipients = opt('mail');
-
- if (opt('mail-failure') && ($result ne 'OK' && $result ne '0')) {
- $recipients = opt('mail-failure');
- }
- if ($msgs && $recipients && $msgs ne $last_msgs) {
- pipecmd("sendmail -oi $recipients",
- "To: $recipients",
- "Subject: status report from $program\@$hostname",
- "\r\n",
- $msgs,
- "",
- "regards,",
- " $program\@$hostname (version $version)"
- );
- }
- $last_msgs = $msgs;
- $msgs = '';
-}
-######################################################################
-## split_by_comma
-## merge
-## default
-## minimum
-## opt
-######################################################################
-sub split_by_comma {
- my $string = shift;
-
- return split /\s*[, ]\s*/, $string if defined $string;
- return ();
-}
-sub merge {
- my %merged = ();
- foreach my $h (@_) {
- foreach my $k (keys %$h) {
- $merged{$k} = $h->{$k} unless exists $merged{$k};
- }
- }
- return \%merged;
-}
-sub default {
- my $v = shift;
- return $variables{'merged'}{$v}{'default'};
-}
-sub minimum {
- my $v = shift;
- return $variables{'merged'}{$v}{'minimum'};
-}
-sub opt {
- my $v = shift;
- my $h = shift;
- return $config{$h}{$v} if defined($h && $config{$h}{$v});
- return $opt{$v} if defined $opt{$v};
- return $globals{$v} if defined $globals{$v};
- return default($v) if defined default($v);
- return undef;
-}
-sub min {
- my $min = shift;
- foreach my $arg (@_) {
- $min = $arg if $arg < $min;
- }
- return $min;
-}
-sub max {
- my $max = shift;
- foreach my $arg (@_) {
- $max = $arg if $arg > $max;
- }
- return $max;
-}
-######################################################################
-## define
-######################################################################
-sub define {
- foreach (@_) {
- return $_ if defined $_;
- }
- return undef;
-}
-######################################################################
-## ynu
-######################################################################
-sub ynu {
- my ($value, $yes, $no, $undef) = @_;
-
- return $no if !defined($value) || !$value;
- return $yes if $value eq '1';
- foreach (qw(yes true)) {
- return $yes if $_ =~ /^$value/i;
- }
- foreach (qw(no false)) {
- return $no if $_ =~ /^$value/i;
- }
- return $undef;
-}
-######################################################################
-## msg
-## debug
-## warning
-## fatal
-######################################################################
-sub _msg {
- my $log = shift;
- my $prefix = shift;
- my $format = shift;
- my $buffer = sprintf $format, @_;
- chomp($buffer);
-
- $prefix = sprintf "%-9s ", $prefix if $prefix;
- if ($file) {
- $prefix .= "file $file";
- $prefix .= ", line $lineno" if $lineno;
- $prefix .= ": ";
- }
- if ($prefix) {
- $buffer = "$prefix$buffer";
- $buffer =~ s/\n/\n$prefix /g;
- }
- $buffer .= "\n";
- print $buffer;
-
- $msgs .= $buffer if $log;
- logger($buffer) if $log;
-
-}
-sub msg { _msg(0, '', @_); }
-sub verbose { _msg(1, @_) if opt('verbose'); }
-sub info { _msg(1, 'INFO:', @_) if opt('verbose'); }
-sub debug { _msg(0, 'DEBUG:', @_) if opt('debug'); }
-sub debug2 { _msg(0, 'DEBUG:', @_) if opt('debug') && opt('verbose');}
-sub warning { _msg(1, 'WARNING:', @_); }
-sub fatal { _msg(1, 'FATAL:', @_); sendmail(); exit(1); }
-sub success { _msg(1, 'SUCCESS:', @_); }
-sub failed { _msg(1, 'FAILED:', @_); $result = 'FAILED'; }
-sub prettytime { return scalar(localtime(shift)); }
-
-sub prettyinterval {
- my $interval = shift;
- use integer;
- my $s = $interval % 60; $interval /= 60;
- my $m = $interval % 60; $interval /= 60;
- my $h = $interval % 24; $interval /= 24;
- my $d = $interval;
-
- my $string = "";
- $string .= "$d day" if $d;
- $string .= "s" if $d > 1;
- $string .= ", " if $string && $h;
- $string .= "$h hour" if $h;
- $string .= "s" if $h > 1;
- $string .= ", " if $string && $m;
- $string .= "$m minute" if $m;
- $string .= "s" if $m > 1;
- $string .= ", " if $string && $s;
- $string .= "$s second" if $s;
- $string .= "s" if $s > 1;
- return $string;
-}
-sub interval {
- my $value = shift;
- if ($value =~ /^(\d+)(seconds|s)/i) {
- $value = $1;
- } elsif ($value =~ /^(\d+)(minutes|m)/i) {
- $value = $1 * 60;
- } elsif ($value =~ /^(\d+)(hours|h)/i) {
- $value = $1 * 60*60;
- } elsif ($value =~ /^(\d+)(days|d)/i) {
- $value = $1 * 60*60*24;
- } elsif ($value !~ /^\d+$/) {
- $value = undef;
- }
- return $value;
-}
-sub interval_expired {
- my ($host, $time, $interval) = @_;
-
- return 1 if !exists $cache{$host};
- return 1 if !exists $cache{$host}{$time} || !$cache{$host}{$time};
- return 1 if !exists $config{$host}{$interval} || !$config{$host}{$interval};
-
- return $now > ($cache{$host}{$time} + $config{$host}{$interval});
-}
-
-
-
-######################################################################
-## check_value
-######################################################################
-sub check_value {
- my ($value, $def) = @_;
- my $type = $def->{'type'};
- my $min = $def->{'minimum'};
- my $required = $def->{'required'};
-
- if (!defined $value && !$required) {
- ;
-
- } 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+$/;
- $value = $min if defined($min) && $value < $min;
-
- } elsif ($type eq T_BOOL) {
- if ($value =~ /^y(es)?$|^t(true)?$|^1$/i) {
- $value = 1;
- } elsif ($value =~ /^n(o)?$|^f(alse)?$|^0$/i) {
- $value = 0;
- } else {
- return undef;
- }
- } elsif ($type eq T_FQDN || $type eq T_OFQDN && $value ne '') {
- $value = lc $value;
- return undef if $value !~ /[^.]\.[^.]/;
-
- } elsif ($type eq T_FQDNP) {
- $value = lc $value;
- return undef if $value !~ /[^.]\.[^.].*(:\d+)?$/;
-
- } elsif ($type eq T_PROTO) {
- $value = lc $value;
- return undef if ! exists $services{$value};
-
- } elsif ($type eq T_USE) {
- $value = lc $value;
- return undef if ! exists $ip_strategies{$value};
-
- } elsif ($type eq T_FILE) {
- return undef if $value eq "";
-
- } elsif ($type eq T_IF) {
- return undef if $value !~ /^[a-zA-Z0-9:._-]+$/;
-
- } elsif ($type eq T_PROG) {
- return undef if $value eq "";
-
- } elsif ($type eq T_LOGIN) {
- return undef if $value eq "";
-
-# } elsif ($type eq T_PASSWD) {
-# return undef if $value =~ /:/;
-
- } elsif ($type eq T_IP) {
- if( !ipv6_match($value) ) {
- return undef if $value !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
- }
- }
- return $value;
-}
-######################################################################
-## encode_base64 - from MIME::Base64
-######################################################################
-sub encode_base64 ($;$) {
- my $res = '';
- my $eol = $_[1];
- $eol = "\n" unless defined $eol;
- pos($_[0]) = 0; # ensure start at the beginning
- while ($_[0] =~ /(.{1,45})/gs) {
- $res .= substr(pack('u', $1), 1);
- chop($res);
- }
- $res =~ tr|` -_|AA-Za-z0-9+/|; # `# help emacs
-
- # fix padding at the end
- my $padding = (3 - length($_[0]) % 3) % 3;
- $res =~ s/.{$padding}$/'=' x $padding/e if $padding;
- $res;
-}
-######################################################################
-## load_ssl_support
-######################################################################
-sub load_ssl_support {
- my $ssl_loaded = eval {require IO::Socket::SSL};
- unless ($ssl_loaded) {
- fatal(<<"EOM");
-Error loading the Perl module IO::Socket::SSL needed for SSL connect.
-On Debian, the package libio-socket-ssl-perl must be installed.
-On Red Hat, the package perl-IO-Socket-SSL must be installed.
-On Alpine, the package perl-io-socket-ssl must be installed.
-EOM
- }
- import IO::Socket::SSL;
- { no warnings; $IO::Socket::SSL::DEBUG = 0; }
-}
-
-######################################################################
-## load_ipv6_support
-######################################################################
-sub load_ipv6_support {
- my $ipv6_loaded = eval {require IO::Socket::INET6};
- unless ($ipv6_loaded) {
- fatal(<<"EOM");
-Error loading the Perl module IO::Socket::INET6 needed for ipv6 connect.
-On Debian, the package libio-socket-inet6-perl must be installed.
-On Red Hat, the package perl-IO-Socket-INET6 must be installed.
-On Alpine, the package perl-io-socket-inet6 must be installed.
-EOM
- }
- import IO::Socket::INET6;
- { no warnings; $IO::Socket::INET6::DEBUG = 0; }
-}
-
-######################################################################
-## load_sha1_support
-######################################################################
-sub load_sha1_support {
- my $why = shift;
- my $sha1_loaded = eval {require Digest::SHA1};
- my $sha_loaded = eval {require Digest::SHA};
- unless ($sha1_loaded || $sha_loaded) {
- fatal(<<"EOM");
-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.
-EOM
- }
- if($sha1_loaded) {
- import Digest::SHA1 (qw/sha1_hex/);
- } elsif($sha_loaded) {
- import Digest::SHA (qw/sha1_hex/);
- }
-}
-######################################################################
-## load_json_support
-######################################################################
-sub load_json_support {
- my $why = shift;
- my $json_loaded = eval {require JSON::PP};
- unless ($json_loaded) {
- fatal(<<"EOM");
-Error loading the Perl module JSON::PP needed for $why update.
-EOM
- }
- import JSON::PP (qw/decode_json/);
-}
-######################################################################
-## geturl
-######################################################################
-sub geturl {
- my $proxy = shift || '';
- my $url = shift || '';
- my $login = shift || '';
- my $password = shift || '';
- my $headers = shift || '';
- my $method = shift || 'GET';
- my $data = shift || '';
- my ($peer, $server, $port, $default_port, $use_ssl);
- my ($sd, $rq, $request, $reply);
-
- debug("proxy = $proxy");
- debug("url = %s", $url);
- ## canonify proxy and url
- my $force_ssl;
- $force_ssl = 1 if ($url =~ /^https:/);
- $proxy =~ s%^https?://%%i;
- $url =~ s%^https?://%%i;
- $server = $url;
- $server =~ s%/.*%%;
- $url = "/" unless $url =~ m%/%;
- $url =~ s%^[^/]*/%%;
-
- debug("server = $server");
- opt('fw') && debug("opt(fw = ",opt('fw'),")");
- $globals{'fw'} && debug("glo fw = $globals{'fw'}");
- #if ( $globals{'ssl'} and $server ne $globals{'fw'} ) {
- ## always omit SSL for connections to local router
- if ( $force_ssl || ($globals{'ssl'} and (caller(1))[3] ne 'main::get_ip') ) {
- $use_ssl = 1;
- $default_port = 443;
- load_ssl_support;
- } else {
- $use_ssl = 0;
- $default_port = 80;
- }
-
- ## determine peer and port to use.
- $peer = $proxy || $server;
- $peer =~ s%/.*%%;
- $port = $peer;
- $port =~ s%^.*:%%;
- $port = $default_port unless $port =~ /^\d+$/;
- $peer =~ s%:.*$%%;
-
- my $to = sprintf "%s%s", $server, $proxy ? " via proxy $peer:$port" : "";
- verbose("CONNECT:", "%s", $to);
-
- $request = "$method ";
- if (!$use_ssl) {
- $request .= "http://$server" if $proxy;
- } else {
- $request .= "https://$server" if $proxy;
- }
- $request .= "/$url HTTP/1.0\n";
- $request .= "Host: $server\n";
-
- my $auth = encode_base64("${login}:${password}", "");
- $request .= "Authorization: Basic $auth\n" if $login || $password;
- $request .= "User-Agent: ${program}/${version}\n";
- if ($data) {
- $request .= "Content-Type: application/x-www-form-urlencoded\n" if ! $headers =~ /^Content-Type: /;
- $request .= "Content-Length: " . length($data) . "\n";
- }
- $request .= "Connection: close\n";
- $request .= "$headers\n";
- $request .= "\n";
- $request .= $data;
-
- ## make sure newlines are for some pedantic proxy servers
- ($rq = $request) =~ s/\n/\r\n/g;
-
- # local $^W = 0;
- $0 = sprintf("%s - connecting to %s port %s", $program, $peer, $port);
- if (! opt('exec')) {
- debug("skipped network connection");
- verbose("SENDING:", "%s", $request);
- } elsif ($use_ssl) {
- $sd = IO::Socket::SSL->new(
- PeerAddr => $peer,
- PeerPort => $port,
- Proto => 'tcp',
- MultiHomed => 1,
- Timeout => opt('timeout'),
- );
- defined $sd or warning("cannot connect to $peer:$port socket: $@ " . IO::Socket::SSL::errstr());
- } elsif ($globals{'ipv6'}) {
- load_ipv6_support;
- $sd = IO::Socket::INET6->new(
- PeerAddr => $peer,
- PeerPort => $port,
- Proto => 'tcp',
- MultiHomed => 1,
- Timeout => opt('timeout'),
- );
- defined $sd or warning("cannot connect to $peer:$port socket: $@");
- } else {
- $sd = IO::Socket::INET->new(
- PeerAddr => $peer,
- PeerPort => $port,
- Proto => 'tcp',
- MultiHomed => 1,
- Timeout => opt('timeout'),
- );
- defined $sd or warning("cannot connect to $peer:$port socket: $@");
- }
-
- if (defined $sd) {
- ## send the request to the http server
- verbose("CONNECTED: ", $use_ssl ? 'using SSL' : 'using HTTP');
- verbose("SENDING:", "%s", $request);
-
- $0 = sprintf("%s - sending to %s port %s", $program, $peer, $port);
- my $result = syswrite $sd, $rq;
- if ($result != length($rq)) {
- warning("cannot send to $peer:$port ($!).");
- } else {
- $0 = sprintf("%s - reading from %s port %s", $program, $peer, $port);
- eval {
- local $SIG{'ALRM'} = sub { die "timeout";};
- alarm(opt('timeout')) if opt('timeout') > 0;
- while ($_ = <$sd>) {
- $0 = sprintf("%s - read from %s port %s", $program, $peer, $port);
- verbose("RECEIVE:", "%s", define($_, ""));
- $reply .= $_ if defined $_;
- }
- if (opt('timeout') > 0) {
- alarm(0);
- }
- };
- close($sd);
-
- if ($@ and $@ =~ /timeout/) {
- warning("TIMEOUT: %s after %s seconds", $to, opt('timeout'));
- $reply = '';
- }
- $reply = '' if !defined $reply;
- }
- }
- $0 = sprintf("%s - closed %s port %s", $program, $peer, $port);
-
- ## during testing simulate reading the URL
- if (opt('test')) {
- my $filename = "$server/$url";
- $filename =~ s|/|%2F|g;
- if (opt('exec')) {
- $reply = save_file("${savedir}$filename", $reply, 'unique');
- } else {
- $reply = load_file("${savedir}$filename");
- }
- }
-
- $reply =~ s/\r//g if defined $reply;
- return $reply;
-}
-######################################################################
-## un_zero_pad
-######################################################################
-sub un_zero_pad {
- my $in_str = shift(@_);
- my @out_str = ();
-
- if ($in_str eq '0.0.0.0') {
- return $in_str;
- }
-
- foreach my $block (split /\./, $in_str) {
- $block =~ s/^0+//;
- if ($block eq '') {
- $block = '0';
- }
- push @out_str, $block;
- }
- return join('.', @out_str);
-}
-######################################################################
-## filter_local
-######################################################################
-sub filter_local {
- my $in_ip = shift(@_);
-
- if ($in_ip eq '0.0.0.0') {
- return $in_ip;
- }
-
- my @guess_local = (
- '^10\.',
- '^172\.(?:1[6-9]|2[0-9]|3[01])\.',
- '^192\.168'
- );
- foreach my $block (@guess_local) {
- if ($in_ip =~ /$block/) {
- return '0.0.0.0';
- }
- }
- return $in_ip;
-}
-######################################################################
-## get_ip
-######################################################################
-sub get_ip {
- my $use = lc shift;
- my $h = shift;
- my ($ip, $arg, $reply, $url, $skip) = (undef, opt($use, $h), '');
- $arg = '' unless $arg;
-
- if ($use eq 'ip') {
- $ip = opt('ip', $h);
- $arg = 'ip';
-
- } elsif ($use eq 'if') {
- $skip = opt('if-skip', $h) || '';
- $reply = `ifconfig $arg 2> /dev/null`;
- $reply = `ip addr list dev $arg 2> /dev/null` if $?;
- $reply = '' if $?;
-
- } elsif ($use eq 'cmd') {
- if ($arg) {
- $skip = opt('cmd-skip', $h) || '';
- $reply = `$arg`;
- $reply = '' if $?;
- }
-
- } elsif ($use eq 'web') {
- $url = opt('web', $h) || '';
- $skip = opt('web-skip', $h) || '';
-
- if (exists $builtinweb{$url}) {
- $skip = $builtinweb{$url}->{'skip'} unless $skip;
- $url = $builtinweb{$url}->{'url'};
- }
- $arg = $url;
-
- if ($url) {
- $reply = geturl(opt('proxy', $h), $url) || '';
- }
-
- } elsif (($use eq 'cisco')) {
- # Stuff added to support Cisco router ip http daemon
- # User fw-login should only have level 1 access to prevent
- # password theft. This is pretty harmless.
- my $queryif = opt('if', $h);
- $skip = opt('fw-skip', $h) || '';
-
- # Convert slashes to protected value "\/"
- $queryif =~ s%\/%\\\/%g;
-
- # Protect special HTML characters (like '?')
- $queryif =~ s/([\?&= ])/sprintf("%%%02x",ord($1))/ge;
-
- $url = "http://".opt('fw', $h)."/level/1/exec/show/ip/interface/brief/${queryif}/CR";
- $reply = geturl('', $url, opt('fw-login', $h), opt('fw-password', $h)) || '';
- $arg = $url;
-
- } elsif (($use eq 'cisco-asa')) {
- # Stuff added to support Cisco ASA ip https daemon
- # User fw-login should only have level 1 access to prevent
- # password theft. This is pretty harmless.
- my $queryif = opt('if', $h);
- $skip = opt('fw-skip', $h) || '';
-
- # Convert slashes to protected value "\/"
- $queryif =~ s%\/%\\\/%g;
-
- # Protect special HTML characters (like '?')
- $queryif =~ s/([\?&= ])/sprintf("%%%02x",ord($1))/ge;
-
- $url = "https://".opt('fw', $h)."/exec/show%20interface%20${queryif}";
- $reply = geturl('', $url, opt('fw-login', $h), opt('fw-password', $h)) || '';
- $arg = $url;
-
- } else {
- $url = opt('fw', $h) || '';
- $skip = opt('fw-skip', $h) || '';
-
- if (exists $builtinfw{$use}) {
- $skip = $builtinfw{$use}->{'skip'} unless $skip;
- $url = "http://${url}" . $builtinfw{$use}->{'url'} unless $url =~ /\//;
- }
- $arg = $url;
-
- if ($url) {
- $reply = geturl('', $url, opt('fw-login', $h), opt('fw-password', $h)) || '';
- }
- }
- if (!defined $reply) {
- $reply = '';
- }
- if ($skip) {
- $skip =~ s/ /\\s/is;
- $reply =~ s/^.*?${skip}//is;
- }
- if ($reply =~ /^.*?\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b.*/is) {
- $ip = $1;
- $ip = un_zero_pad($ip);
- $ip = filter_local($ip) if opt('fw-banlocal', $h);
- } elsif ( $ip = ipv6_match($reply) ) {
- $ip = un_zero_pad($ip);
- $ip = filter_local($ip) if opt('fw-banlocal', $h);
- } else {
- warning("found neither ipv4 nor ipv6 address");
- }
- if (($use ne 'ip') && (define($ip,'') eq '0.0.0.0')) {
- $ip = undef;
- }
-
- debug("get_ip: using %s, %s reports %s", $use, $arg, define($ip, ""));
- return $ip;
-}
-
-######################################################################
-## ipv6_match determine ipv6 address from given string and return them
-######################################################################
-sub ipv6_match {
- my $content = shift;
- my $omits;
- my $ip = "";
- my $linenumbers = 0;
-
- my @values = split('\n', $content);
- foreach my $val (@values) {
- next unless $val =~ /((:{0,2}[A-F0-9]{1,4}){0,7}:{1,2}[A-F0-9]{1,4})/ai; # invalid char
- my $parsed = $1;
-
- # check for at least 7 colons
- my $count_colon = () = $parsed =~ /:/g;
- if ($count_colon != 7) {
- # or one double colon
- my $count_double_colon = () = $parsed =~ /::/g;
- if ($count_double_colon != 1) {
- next
- }
- }
- return $parsed;
- }
- return;
-}
-
-######################################################################
-## group_hosts_by
-######################################################################
-sub group_hosts_by {
- my ($hosts, $attributes) = @_;
-
- my %groups = ();
- foreach my $h (@$hosts) {
- my @keys = (@$attributes, 'wantip');
- map { $config{$h}{$_} = '' unless exists $config{$h}{$_} } @keys;
- my $sig = join(',', map { "$_=$config{$h}{$_}" } @keys);
-
- push @{$groups{$sig}}, $h;
- }
- 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
-######################################################################
-sub nic_examples {
- my $examples = "";
- my $separator = "";
- foreach my $s (sort keys %services) {
- my $subr = $services{$s}{'examples'};
- my $example;
-
- if (defined($subr) && ($example = &$subr())) {
- chomp($example);
- $examples .= $example;
- $examples .= "\n\n$separator";
- $separator = "\n";
- }
- }
- my $intro = < $now) {
- warning("cannot update %s from %s to %s until after %s.",
- $host,
- ($cache{$host}{'ip'} ? $cache{$host}{'ip'} : ''), $ip,
- prettytime($cache{$host}{'wtime'})
- );
-
- } elsif ($cache{$host}{'mtime'} && interval_expired($host, 'mtime', 'max-interval')) {
- warning("forcing update of %s from %s to %s; %s since last update on %s.",
- $host,
- ($cache{$host}{'ip'} ? $cache{$host}{'ip'} : ''), $ip,
- prettyinterval($config{$host}{'max-interval'}),
- prettytime($cache{$host}{'mtime'})
- );
- $update = 1;
-
- } elsif ((!exists($cache{$host}{'ip'})) ||
- ("$cache{$host}{'ip'}" ne "$ip")) {
- if (($cache{$host}{'status'} eq 'good') &&
- !interval_expired($host, 'mtime', 'min-interval')) {
-
- warning("skipping update of %s from %s to %s.\nlast updated %s.\nWait at least %s between update attempts.",
- $host,
- ($cache{$host}{'ip'} ? $cache{$host}{'ip'} : ''),
- $ip,
- ($cache{$host}{'mtime'} ? prettytime($cache{$host}{'mtime'}) : ''),
- prettyinterval($config{$host}{'min-interval'})
- )
- if opt('verbose') || !define($cache{$host}{'warned-min-interval'}, 0);
-
- $cache{$host}{'warned-min-interval'} = $now;
-
- } elsif (($cache{$host}{'status'} ne 'good') && !interval_expired($host, 'atime', 'min-error-interval')) {
-
- warning("skipping update of %s from %s to %s.\nlast updated %s but last attempt on %s failed.\nWait at least %s between update attempts.",
- $host,
- ($cache{$host}{'ip'} ? $cache{$host}{'ip'} : ''),
- $ip,
- ($cache{$host}{'mtime'} ? prettytime($cache{$host}{'mtime'}) : ''),
- ($cache{$host}{'atime'} ? prettytime($cache{$host}{'atime'}) : ''),
- prettyinterval($config{$host}{'min-error-interval'})
- )
- if opt('verbose') || !define($cache{$host}{'warned-min-error-interval'}, 0);
-
- $cache{$host}{'warned-min-error-interval'} = $now;
-
- } else {
- $update = 1;
- }
-
- } elsif (defined($sub) && &$sub($host)) {
- $update = 1;
- } elsif ((defined($cache{$host}{'static'}) && defined($config{$host}{'static'}) &&
- ($cache{$host}{'static'} ne $config{$host}{'static'})) ||
- (defined($cache{$host}{'wildcard'}) && defined($config{$host}{'wildcard'}) &&
- ($cache{$host}{'wildcard'} ne $config{$host}{'wildcard'})) ||
- (defined($cache{$host}{'mx'}) && defined($config{$host}{'mx'}) &&
- ($cache{$host}{'mx'} ne $config{$host}{'mx'})) ||
- (defined($cache{$host}{'backupmx'}) && defined($config{$host}{'backupmx'}) &&
- ($cache{$host}{'backupmx'} ne $config{$host}{'backupmx'})) ) {
- info("updating %s because host settings have been changed.", $host);
- $update = 1;
-
- } else {
- success("%s: skipped: IP address was already set to %s.", $host, $ip)
- if opt('verbose');
- }
- $config{$host}{'status'} = define($cache{$host}{'status'},'');
- $config{$host}{'update'} = $update;
- if ($update) {
- $config{$host}{'status'} = 'noconnect';
- $config{$host}{'atime'} = $now;
- $config{$host}{'wtime'} = 0;
- $config{$host}{'warned-min-interval'} = 0;
- $config{$host}{'warned-min-error-interval'} = 0;
-
- delete $cache{$host}{'warned-min-interval'};
- delete $cache{$host}{'warned-min-error-interval'};
- }
-
- return $update;
-}
-######################################################################
-## header_ok
-######################################################################
-sub header_ok {
- my ($host, $line) = @_;
- my $ok = 0;
-
- if ($line =~ m%^s*HTTP/1.*\s+(\d+)%i) {
- my $result = $1;
-
- if ($result eq '200') {
- $ok = 1;
-
- } elsif ($result eq '401') {
- failed("updating %s: authorization failed (%s)", $host, $line);
- }
-
- } else {
- failed("updating %s: unexpected line (%s)", $host, $line);
- }
- return $ok;
-}
-######################################################################
-## nic_dyndns1_examples
-######################################################################
-sub nic_dyndns1_examples {
- return <\s*(.*)\s*%i;
- $return_code = $1 if $line =~ m%^return\s+code\s*:\s*(.*)\s*$%i;
- $error_code = $1 if $line =~ m%^error\s+code\s*:\s*(.*)\s*$%i;
- }
-
- if ($return_code ne 'NOERROR' || $error_code ne 'NOERROR' || !$title) {
- $config{$h}{'status'} = 'failed';
- $title = "incomplete response from $config{$h}{server}" unless $title;
- warning("SENT: %s", $url) unless opt('verbose');
- warning("REPLIED: %s", $reply);
- failed("updating %s: %s", $h, $title);
-
- } else {
- $config{$h}{'ip'} = $ip;
- $config{$h}{'mtime'} = $now;
- $config{$h}{'status'} = 'good';
- success("updating %s: %s: IP address set to %s (%s)", $h, $return_code, $ip, $title);
- }
- }
-}
-######################################################################
-## nic_dyndns2_updateable
-######################################################################
-sub nic_dyndns2_updateable {
- my $host = shift;
- my $update = 0;
-
- if ($config{$host}{'mx'} ne $cache{$host}{'mx'}) {
- info("forcing updating %s because 'mx' has changed to %s.", $host, $config{$host}{'mx'});
- $update = 1;
-
- } elsif ($config{$host}{'mx'} && (ynu($config{$host}{'backupmx'},1,2,3) ne ynu($config{$host}{'backupmx'},1,2,3))) {
- info("forcing updating %s because 'backupmx' has changed to %s.", $host, ynu($config{$host}{'backupmx'},"YES","NO","NO"));
- $update = 1;
-
- } elsif ($config{$host}{'static'} ne $cache{$host}{'static'}) {
-
- info("forcing updating %s because 'static' has changed to %s.", $host, ynu($config{$host}{'static'},"YES","NO","NO"));
- $update = 1;
-
- }
- return $update;
-}
-######################################################################
-## nic_dyndns2_examples
-######################################################################
-sub nic_dyndns2_examples {
- return < 'Bad authorization (username or password)',
- 'badsys' => 'The system parameter given was not valid',
-
- 'notfqdn' => 'A Fully-Qualified Domain Name was not provided',
- 'nohost' => 'The hostname specified does not exist in the database',
- '!yours' => 'The hostname specified exists, but not under the username currently being used',
- '!donator' => 'The offline setting was set, when the user is not a donator',
- '!active' => 'The hostname specified is in a Custom DNS domain which has not yet been activated.',
- 'abuse', => 'The hostname specified is blocked for abuse; you should receive an email notification ' .
- 'which provides an unblock request link. More info can be found on ' .
- 'https://www.dyndns.com/support/abuse.html',
-
- 'numhost' => 'System error: Too many or too few hosts found. Contact support@dyndns.org',
- 'dnserr' => 'System error: DNS error encountered. Contact support@dyndns.org',
-
- 'nochg' => 'No update required; unnecessary attempts to change to the current address are considered abusive',
- );
-
- ## update each set of hosts that had similar configurations
- foreach my $sig (keys %groups) {
- my @hosts = @{$groups{$sig}};
- my $hosts = join(',', @hosts);
- my $h = $hosts[0];
- my $ip = $config{$h}{'wantip'};
- delete $config{$_}{'wantip'} foreach @hosts;
-
- info("setting IP address to %s for %s", $ip, $hosts);
- verbose("UPDATE:","updating %s", $hosts);
-
- ## Select the DynDNS system to update
- my $url = "http://$config{$h}{'server'}$config{$h}{'script'}?system=";
- if ($config{$h}{'custom'}) {
- warning("updating %s: 'custom' and 'static' may not be used together. ('static' ignored)", $hosts)
- if $config{$h}{'static'};
-# warning("updating %s: 'custom' and 'offline' may not be used together. ('offline' ignored)", $hosts)
-# if $config{$h}{'offline'};
- $url .= 'custom';
-
- } elsif ($config{$h}{'static'}) {
-# warning("updating %s: 'static' and 'offline' may not be used together. ('offline' ignored)", $hosts)
-# if $config{$h}{'offline'};
- $url .= 'statdns';
-
- } else {
- $url .= 'dyndns';
- }
-
- $url .= "&hostname=$hosts";
- $url .= "&myip=";
- $url .= $ip if $ip;
-
- ## some args are not valid for a custom domain.
- $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');
- }
-
- my $reply = geturl(opt('proxy'), $url, $config{$h}{'login'}, $config{$h}{'password'});
- if (!defined($reply) || !$reply) {
- failed("updating %s: Could not connect to %s.", $hosts, $config{$h}{'server'});
- last;
- }
- last if !header_ok($hosts, $reply);
-
- my @reply = split /\n/, $reply;
- my $state = 'header';
- my $returnedip = $ip;
-
- foreach my $line (@reply) {
- if ($state eq 'header') {
- $state = 'body';
-
- } elsif ($state eq 'body') {
- $state = 'results' if $line eq '';
-
- } elsif ($state =~ /^results/) {
- $state = 'results2';
-
- # bug #10: some dyndns providers does not return the IP so
- # we can't use the returned IP
- my ($status, $returnedip) = split / /, lc $line;
- $ip = $returnedip if (not $ip);
- my $h = shift @hosts;
-
- $config{$h}{'status'} = $status;
- if ($status eq 'good') {
- $config{$h}{'ip'} = $ip;
- $config{$h}{'mtime'} = $now;
- success("updating %s: %s: IP address set to %s", $h, $status, $ip);
-
- } elsif (exists $errors{$status}) {
- if ($status eq 'nochg') {
- warning("updating %s: %s: %s", $h, $status, $errors{$status});
- $config{$h}{'ip'} = $ip;
- $config{$h}{'mtime'} = $now;
- $config{$h}{'status'} = 'good';
-
- } else {
- failed("updating %s: %s: %s", $h, $status, $errors{$status});
- }
-
- } elsif ($status =~ /w(\d+)(.)/) {
- my ($wait, $units) = ($1, lc $2);
- my ($sec, $scale) = ($wait, 1);
-
- ($scale, $units) = (1, 'seconds') if $units eq 's';
- ($scale, $units) = (60, 'minutes') if $units eq 'm';
- ($scale, $units) = (60*60, 'hours') if $units eq 'h';
-
- $sec = $wait * $scale;
- $config{$h}{'wtime'} = $now + $sec;
- warning("updating %s: %s: wait $wait $units before further updates", $h, $status, $ip);
-
- } else {
- failed("updating %s: %s: unexpected status (%s)", $h, $line);
- }
- }
- }
- failed("updating %s: Could not connect to %s.", $hosts, $config{$h}{'server'})
- if $state ne 'results2';
- }
-}
-
-
-######################################################################
-## nic_noip_update
-## Note: uses same features as nic_dyndns2_update, less return codes
-######################################################################
-sub nic_noip_update {
- debug("\nnic_noip_update -------------------");
-
- ## group hosts with identical attributes together
- my %groups = group_hosts_by([ @_ ], [ qw(login password server static custom wildcard mx backupmx) ]);
-
- my %errors = (
- 'badauth' => 'Invalid username or password',
- 'badagent' => 'Invalid user agent',
- 'nohost' => 'The hostname specified does not exist in the database',
- '!donator' => 'The offline setting was set, when the user is not a donator',
- 'abuse', => 'The hostname specified is blocked for abuse; open a trouble ticket at http://www.no-ip.com',
- 'numhost' => 'System error: Too many or too few hosts found. open a trouble ticket at http://www.no-ip.com',
- 'dnserr' => 'System error: DNS error encountered. Contact support@dyndns.org',
- 'nochg' => 'No update required; unnecessary attempts to change to the current address are considered abusive',
- );
-
- ## update each set of hosts that had similar configurations
- foreach my $sig (keys %groups) {
- my @hosts = @{$groups{$sig}};
- my $hosts = join(',', @hosts);
- my $h = $hosts[0];
- my $ip = $config{$h}{'wantip'};
- delete $config{$_}{'wantip'} foreach @hosts;
-
- info("setting IP address to %s for %s", $ip, $hosts);
- verbose("UPDATE:","updating %s", $hosts);
-
- my $url = "http://$config{$h}{'server'}/nic/update?system=";
- $url .= 'noip';
- $url .= "&hostname=$hosts";
- $url .= "&myip=";
- $url .= $ip if $ip;
-
- my $reply = geturl(opt('proxy'), $url, $config{$h}{'login'}, $config{$h}{'password'});
- if (!defined($reply) || !$reply) {
- failed("updating %s: Could not connect to %s.", $hosts, $config{$h}{'server'});
- last;
- }
- last if !header_ok($hosts, $reply);
-
- my @reply = split /\n/, $reply;
- my $state = 'header';
- foreach my $line (@reply) {
- if ($state eq 'header') {
- $state = 'body';
-
- } elsif ($state eq 'body') {
- $state = 'results' if $line eq '';
-
- } elsif ($state =~ /^results/) {
- $state = 'results2';
-
- my ($status, $ip) = split / /, lc $line;
- my $h = shift @hosts;
-
- $config{$h}{'status'} = $status;
- if ($status eq 'good') {
- $config{$h}{'ip'} = $ip;
- $config{$h}{'mtime'} = $now;
- success("updating %s: %s: IP address set to %s", $h, $status, $ip);
-
- } elsif (exists $errors{$status}) {
- if ($status eq 'nochg') {
- warning("updating %s: %s: %s", $h, $status, $errors{$status});
- $config{$h}{'ip'} = $ip;
- $config{$h}{'mtime'} = $now;
- $config{$h}{'status'} = 'good';
-
- } else {
- failed("updating %s: %s: %s", $h, $status, $errors{$status});
- }
-
- } elsif ($status =~ /w(\d+)(.)/) {
- my ($wait, $units) = ($1, lc $2);
- my ($sec, $scale) = ($wait, 1);
-
- ($scale, $units) = (1, 'seconds') if $units eq 's';
- ($scale, $units) = (60, 'minutes') if $units eq 'm';
- ($scale, $units) = (60*60, 'hours') if $units eq 'h';
-
- $sec = $wait * $scale;
- $config{$h}{'wtime'} = $now + $sec;
- warning("updating %s: %s: wait $wait $units before further updates", $h, $status, $ip);
-
- } else {
- failed("updating %s: %s: unexpected status (%s)", $h, $line);
- }
- }
- }
- failed("updating %s: Could not connect to %s.", $hosts, $config{$h}{'server'})
- if $state ne 'results2';
- }
-}
-######################################################################
-## nic_noip_examples
-######################################################################
-sub nic_noip_examples {
- return </i, @reply) {
- $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';
- warning("SENT: %s", $url) unless opt('verbose');
- warning("REPLIED: %s", $reply);
- failed("updating %s: Invalid reply.", $h);
- }
- }
-}
-######################################################################
-## nic_zoneedit1_examples
-######################################################################
-sub nic_zoneedit1_examples {
- return <
-#
-#
-######################################################################
-sub nic_zoneedit1_update {
- debug("\nnic_zoneedit1_update -------------------");
-
- ## group hosts with identical attributes together
- my %groups = group_hosts_by([ @_ ], [ qw(login password server zone) ]);
-
- ## update each set of hosts that had similar configurations
- foreach my $sig (keys %groups) {
- my @hosts = @{$groups{$sig}};
- my $hosts = join(',', @hosts);
- my $h = $hosts[0];
- my $ip = $config{$h}{'wantip'};
- delete $config{$_}{'wantip'} foreach @hosts;
-
- info("setting IP address to %s for %s", $ip, $hosts);
- verbose("UPDATE:","updating %s", $hosts);
-
- my $url = '';
- $url .= "http://$config{$h}{'server'}/auth/dynamic.html";
- $url .= "?host=$hosts";
- $url .= "&dnsto=$ip" if $ip;
- $url .= "&zone=$config{$h}{'zone'}" if defined $config{$h}{'zone'};
-
- my $reply = geturl(opt('proxy'), $url, $config{$h}{'login'}, $config{$h}{'password'});
- if (!defined($reply) || !$reply) {
- failed("updating %s: Could not connect to %s.", $hosts, $config{$h}{'server'});
- last;
- }
- last if !header_ok($hosts, $reply);
-
- my @reply = split /\n/, $reply;
- foreach my $line (@reply) {
- if ($line =~ /^[^<]*<(SUCCESS|ERROR)\s+([^>]+)>(.*)/) {
- my ($status, $assignments, $rest) = ($1, $2, $3);
- my ($left, %var) = parse_assignments($assignments);
-
- if (keys %var) {
- my ($status_code, $status_text, $status_ip) = ('999', '', $ip);
- $status_code = $var{'CODE'} if exists $var{'CODE'};
- $status_text = $var{'TEXT'} if exists $var{'TEXT'};
- $status_ip = $var{'IP'} if exists $var{'IP'};
-
- if ($status eq 'SUCCESS' || ($status eq 'ERROR' && $var{'CODE'} eq '707')) {
- $config{$h}{'ip'} = $status_ip;
- $config{$h}{'mtime'} = $now;
- $config{$h}{'status'} = 'good';
-
- success("updating %s: IP address set to %s (%s: %s)", $h, $ip, $status_code, $status_text);
-
- } else {
- $config{$h}{'status'} = 'failed';
- failed("updating %s: %s: %s", $h, $status_code, $status_text);
- }
- shift @hosts;
- $h = $hosts[0];
- $hosts = join(',', @hosts);
- }
- $line = $rest;
- redo if $line;
- }
- }
- failed("updating %s: no response from %s", $hosts, $config{$h}{'server'})
- if @hosts;
- }
-}
-######################################################################
-## nic_easydns_updateable
-######################################################################
-sub nic_easydns_updateable {
- my $host = shift;
- my $update = 0;
-
- if ($config{$host}{'mx'} ne $cache{$host}{'mx'}) {
- info("forcing updating %s because 'mx' has changed to %s.", $host, $config{$host}{'mx'});
- $update = 1;
-
- } elsif ($config{$host}{'mx'} && (ynu($config{$host}{'backupmx'},1,2,3) ne ynu($config{$host}{'backupmx'},1,2,3))) {
- info("forcing updating %s because 'backupmx' has changed to %s.", $host, ynu($config{$host}{'backupmx'},"YES","NO","NO"));
- $update = 1;
-
- } elsif ($config{$host}{'static'} ne $cache{$host}{'static'}) {
-
- info("forcing updating %s because 'static' has changed to %s.", $host, ynu($config{$host}{'static'},"YES","NO","NO"));
- $update = 1;
-
- }
- return $update;
-}
-######################################################################
-## nic_easydns_examples
-######################################################################
-sub nic_easydns_examples {
- return < [ $_ ] } @_;
-
- my %errors = (
- 'NOACCESS' => 'Authentication failed. This happens if the username/password OR host or domain are wrong.',
- 'NOSERVICE'=> 'Dynamic DNS is not turned on for this domain.',
- 'ILLEGAL' => 'Client sent data that is not allowed in a dynamic DNS update.',
- 'TOOSOON' => 'Update frequency is too short.',
- );
-
- ## update each set of hosts that had similar configurations
- foreach my $sig (keys %groups) {
- my @hosts = @{$groups{$sig}};
- my $hosts = join(',', @hosts);
- my $h = $hosts[0];
- my $ip = $config{$h}{'wantip'};
- delete $config{$_}{'wantip'} foreach @hosts;
-
- info("setting IP address to %s for %s", $ip, $hosts);
- verbose("UPDATE:","updating %s", $hosts);
-
- #'http://members.easydns.com/dyn/dyndns.php?hostname=test.burry.ca&myip=10.20.30.40&wildcard=ON'
-
- my $url;
- $url = "http://$config{$h}{'server'}/dyn/dyndns.php?";
- $url .= "hostname=$hosts";
- $url .= "&myip=";
- $url .= $ip if $ip;
- $url .= "&wildcard=" . ynu($config{$h}{'wildcard'}, 'ON', 'OFF', 'OFF') if defined $config{$h}{'wildcard'};
-
- if ($config{$h}{'mx'}) {
- $url .= "&mx=$config{$h}{'mx'}";
- $url .= "&backmx=" . ynu($config{$h}{'backupmx'}, 'YES', 'NO');
- }
-
- my $reply = geturl(opt('proxy'), $url, $config{$h}{'login'}, $config{$h}{'password'});
- if (!defined($reply) || !$reply) {
- failed("updating %s: Could not connect to %s.", $hosts, $config{$h}{'server'});
- last;
- }
- last if !header_ok($hosts, $reply);
-
- my @reply = split /\n/, $reply;
- my $state = 'header';
- foreach my $line (@reply) {
- if ($state eq 'header') {
- $state = 'body';
-
- } elsif ($state eq 'body') {
- $state = 'results' if $line eq '';
-
- } elsif ($state =~ /^results/) {
- $state = 'results2';
-
- my ($status) = $line =~ /^(\S*)\b.*/;
- my $h = shift @hosts;
-
- $config{$h}{'status'} = $status;
- if ($status eq 'NOERROR') {
- $config{$h}{'ip'} = $ip;
- $config{$h}{'mtime'} = $now;
- success("updating %s: %s: IP address set to %s", $h, $status, $ip);
-
- } elsif ($status =~ /TOOSOON/) {
- ## make sure we wait at least a little
- my ($wait, $units) = (5, 'm');
- my ($sec, $scale) = ($wait, 1);
-
- ($scale, $units) = (1, 'seconds') if $units eq 's';
- ($scale, $units) = (60, 'minutes') if $units eq 'm';
- ($scale, $units) = (60*60, 'hours') if $units eq 'h';
- $config{$h}{'wtime'} = $now + $sec;
- warning("updating %s: %s: wait $wait $units before further updates", $h, $status, $ip);
-
- } elsif (exists $errors{$status}) {
- failed("updating %s: %s: %s", $h, $line, $errors{$status});
-
- } else {
- failed("updating %s: %s: unexpected status (%s)", $h, $line);
- }
- last;
- }
- }
- failed("updating %s: Could not connect to %s.", $hosts, $config{$h}{'server'})
- if $state ne 'results2';
- }
-}
-######################################################################
-
-######################################################################
-## nic_dnspark_updateable
-######################################################################
-sub nic_dnspark_updateable {
- my $host = shift;
- my $update = 0;
-
- if ($config{$host}{'mx'} ne $cache{$host}{'mx'}) {
- info("forcing updating %s because 'mx' has changed to %s.", $host, $config{$host}{'mx'});
- $update = 1;
-
- } elsif ($config{$host}{'mx'} && ($config{$host}{'mxpri'} ne $cache{$host}{'mxpri'})) {
- info("forcing updating %s because 'mxpri' has changed to %s.", $host, $config{$host}{'mxpri'});
- $update = 1;
- }
- return $update;
-}
-######################################################################
-## nic_dnspark_examples
-######################################################################
-sub nic_dnspark_examples {
- return < [ $_ ] } @_;
-
- my %errors = (
- 'nochange' => 'No changes made to the hostname(s). Continual updates with no changes lead to blocked clients.',
- 'nofqdn' => 'No valid FQDN (fully qualified domain name) was specified',
- 'nohost'=> 'An invalid hostname was specified. This due to the fact the hostname has not been created in the system. Creating new host names via clients is not supported.',
- 'abuse' => 'The hostname specified has been blocked for abuse.',
- 'unauth' => 'The username specified is not authorized to update this hostname and domain.',
- 'blocked' => 'The dynamic update client (specified by the user-agent) has been blocked from the system.',
- 'notdyn' => 'The hostname specified has not been marked as a dynamic host. Hosts must be marked as dynamic in the system in order to be updated via clients. This prevents unwanted or accidental updates.',
- );
-
- ## update each set of hosts that had similar configurations
- foreach my $sig (keys %groups) {
- my @hosts = @{$groups{$sig}};
- my $hosts = join(',', @hosts);
- my $h = $hosts[0];
- my $ip = $config{$h}{'wantip'};
- delete $config{$_}{'wantip'} foreach @hosts;
-
- info("setting IP address to %s for %s", $ip, $hosts);
- verbose("UPDATE:","updating %s", $hosts);
-
- #'http://www.dnspark.com:80/visitors/update.html?myip=10.20.30.40&hostname=test.burry.ca'
-
- my $url;
- $url = "http://$config{$h}{'server'}/visitors/update.html";
- $url .= "?hostname=$hosts";
- $url .= "&myip=";
- $url .= $ip if $ip;
-
- if ($config{$h}{'mx'}) {
- $url .= "&mx=$config{$h}{'mx'}";
- $url .= "&mxpri=" . $config{$h}{'mxpri'};
- }
-
- my $reply = geturl(opt('proxy'), $url, $config{$h}{'login'}, $config{$h}{'password'});
- if (!defined($reply) || !$reply) {
- failed("updating %s: Could not connect to %s.", $hosts, $config{$h}{'server'});
- last;
- }
- last if !header_ok($hosts, $reply);
-
- my @reply = split /\n/, $reply;
- my $state = 'header';
- foreach my $line (@reply) {
- if ($state eq 'header') {
- $state = 'body';
-
- } elsif ($state eq 'body') {
- $state = 'results' if $line eq '';
-
- } elsif ($state =~ /^results/) {
- $state = 'results2';
-
- my ($status) = $line =~ /^(\S*)\b.*/;
- my $h = pop @hosts;
-
- $config{$h}{'status'} = $status;
- if ($status eq 'ok') {
- $config{$h}{'ip'} = $ip;
- $config{$h}{'mtime'} = $now;
- success("updating %s: %s: IP address set to %s", $h, $status, $ip);
-
- } elsif ($status =~ /TOOSOON/) {
- ## make sure we wait at least a little
- my ($wait, $units) = (5, 'm');
- my ($sec, $scale) = ($wait, 1);
-
- ($scale, $units) = (1, 'seconds') if $units eq 's';
- ($scale, $units) = (60, 'minutes') if $units eq 'm';
- ($scale, $units) = (60*60, 'hours') if $units eq 'h';
- $config{$h}{'wtime'} = $now + $sec;
- warning("updating %s: %s: wait $wait $units before further updates", $h, $status, $ip);
-
- } elsif (exists $errors{$status}) {
- failed("updating %s: %s: %s", $h, $line, $errors{$status});
-
- } else {
- failed("updating %s: %s: unexpected status (%s)", $h, $line);
- }
- last;
- }
- }
- failed("updating %s: Could not connect to %s.", $hosts, $config{$h}{'server'})
- if $state ne 'results2';
- }
-}
-
-######################################################################
-
-######################################################################
-## nic_namecheap_examples
-######################################################################
-sub nic_namecheap_examples {
- return <0/i, @reply) {
- $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';
- warning("SENT: %s", $url) unless opt('verbose');
- warning("REPLIED: %s", $reply);
- failed("updating %s: Invalid reply.", $h);
- }
- }
-}
-
-######################################################################
-
-######################################################################
-## nic_nfsn_examples
-######################################################################
-sub nic_nfsn_examples {
- return <{'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 = $h;
- $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);
- }
- }
-}
-
-######################################################################
-
-######################################################################
-## nic_sitelutions_examples
-######################################################################
-sub nic_sitelutions_examples {
- return <
-## This returns a list of host|currentIP|updateURL lines.
-## Pick the line that matches myhost, and fetch the URL.
-## word 'Updated' for success, 'fail' for failure.
-##
-######################################################################
-sub nic_freedns_update {
-
-
- debug("\nnic_freedns_update -------------------");
-
- ## First get the list of updatable hosts
- my $url;
- $url = "http://$config{$_[0]}{'server'}/api/?action=getdyndns&sha=".&sha1_hex("$config{$_[0]}{'login'}|$config{$_[0]}{'password'}");
- my $reply = geturl(opt('proxy'), $url);
- if (!defined($reply) || !$reply || !header_ok($_[0], $reply)) {
- failed("updating %s: Could not connect to %s for site list.", $_[0], $url);
- return;
- }
- my @lines = split("\n", $reply);
- my %freedns_hosts;
- grep {
- my @rec = split(/\|/, $_);
- $freedns_hosts{$rec[0]} = \@rec if ($#rec > 0);
- } @lines;
- if (!keys %freedns_hosts) {
- failed("Could not get freedns update URLs from %s", $config{$_[0]}{'server'});
- return;
- }
- ## update each configured host
- foreach my $h (@_) {
- if(!$h){ next };
- my $ip = delete $config{$h}{'wantip'};
- info("setting IP address to %s for %s", $ip, $h);
- verbose("UPDATE:","updating %s", $h);
-
- if($ip eq $freedns_hosts{$h}->[1]) {
- $config{$h}{'ip'} = $ip;
- $config{$h}{'mtime'} = $now;
- $config{$h}{'status'} = 'good';
- success("update not necessary %s: good: IP address already set to %s", $h, $ip);
- } else {
- my $reply = geturl(opt('proxy'), $freedns_hosts{$h}->[2]);
- if (!defined($reply) || !$reply) {
- failed("updating %s: Could not connect to %s.", $h, $freedns_hosts{$h}->[2]);
- last;
- }
- if(!header_ok($h, $reply)) {
- $config{$h}{'status'} = 'failed';
- last;
- }
-
- if($reply =~ /Updated.*$h.*to.*$ip/) {
- $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';
- warning("SENT: %s", $freedns_hosts{$h}->[2]) unless opt('verbose');
- warning("REPLIED: %s", $reply);
- failed("updating %s: Invalid reply.", $h);
- }
- }
- }
-}
-
-######################################################################
-## nic_changeip_examples
-######################################################################
-sub nic_changeip_examples {
-return < ## fully qualified hostname to update
-
-Example ${program}.conf file entries:
- ## single host update
- protocol=nsupdate \\
- server=ns1.example.com \\
- password=/etc/${program}/dyn.example.com.key \\
- zone=dyn.example.com \\
- ttl=3600 \\
- myhost.dyn.example.com
-
-EoEXAMPLE
-}
-
-######################################################################
-## nic_nsupdate_update
-## by Daniel Roethlisberger
-######################################################################
-sub nic_nsupdate_update {
- debug("\nnic_nsupdate_update -------------------");
-
- ## group hosts with identical attributes together
- my %groups = group_hosts_by([ @_ ], [ qw(login password server zone) ]);
-
- ## update each set of hosts that had similar configurations
- foreach my $sig (keys %groups) {
- my @hosts = @{$groups{$sig}};
- my $hosts = join(',', @hosts);
- my $h = $hosts[0];
- my $binary = $config{$h}{'login'};
- my $keyfile = $config{$h}{'password'};
- my $server = $config{$h}{'server'};
- ## nsupdate requires a port number to be separated by whitepace, not colon
- $server =~ s/:/ /;
- my $zone = $config{$h}{'zone'};
- my $ip = $config{$h}{'wantip'};
- my $recordtype = '';
- if (is_ipv6($ip)) {
- $recordtype = 'AAAA';
- } else {
- $recordtype = 'A';
- }
- delete $config{$_}{'wantip'} foreach @hosts;
-
- info("setting IP address to %s for %s", $ip, $hosts);
- verbose("UPDATE:","updating %s", $hosts);
-
- ## send separate requests for each zone with all hosts in that zone
- my $instructions = <{result}) {
- failed ("invalid json or result.");
- next;
- }
-
- # Pull the ID out of the json, messy
- my ($zone_id) = map { $_->{name} eq $config{$key}{'zone'} ? $_->{id} : () } @{ $response->{result} };
- unless($zone_id) {
- failed("updating %s: No zone ID found.", $config{$key}{'zone'});
- next;
- }
- info("zone ID is $zone_id");
-
- # Get DNS record ID
- $url = "https://$config{$key}{'server'}/zones/$zone_id/dns_records?";
- if (is_ipv6($ip)) {
- $url .= "type=AAAA&name=$domain";
- } else {
- $url .= "type=A&name=$domain";
- }
-
- $reply = geturl(opt('proxy'), $url, undef, undef, $headers);
- unless ($reply) {
- failed("updating %s: Could not connect to %s.", $domain, $config{$key}{'server'});
- last;
- }
- last if !header_ok($domain, $reply);
-
- # Strip header
- $reply =~ s/^.*?\n\n//s;
- $response = eval {decode_json($reply)};
- if (!defined $response || !defined $response->{result}) {
- failed ("invalid json or result.");
- next;
- }
-
- # Pull the ID out of the json, messy
- my ($dns_rec_id) = map { $_->{name} eq $domain ? $_->{id} : () } @{ $response->{result} };
- unless($dns_rec_id) {
- failed("updating %s: No DNS record ID found.", $domain);
- next;
- }
- info("DNS record ID is $dns_rec_id");
-
- # Set domain
- $url = "https://$config{$key}{'server'}/zones/$zone_id/dns_records/$dns_rec_id";
- my $data = "{\"content\":\"$ip\"}";
- $reply = geturl(opt('proxy'), $url, undef, undef, $headers, "PATCH", $data);
- unless ($reply) {
- failed("updating %s: Could not connect to %s.", $domain, $config{$domain}{'server'});
- last;
- }
- last if !header_ok($domain, $reply);
-
- # Strip header
- $reply =~ s/^.*?\n\n//s;
- $response = eval {decode_json($reply)};
- if (!defined $response || !defined $response->{result}) {
- failed ("invalid json or result.");
- } else {
- success ("%s -- Updated Successfully to %s", $domain, $ip);
-
- }
-
- # Cache
- $config{$key}{'ip'} = $ip;
- $config{$key}{'mtime'} = $now;
- $config{$key}{'status'} = 'good';
- }
- }
-}
-######################################################################
-## nic_yandex_examples
-######################################################################
-sub nic_yandex_examples {
- return <{success} eq 'error') {
- failed ("%s", $response->{error});
- next;
- }
-
- # Pull the ID out of the json
- my ($id) = map { $_->{fqdn} eq $host ? $_->{record_id} : () } @{ $response->{records} };
- unless($id) {
- failed("updating %s: DNS record ID not found.", $host);
- next;
- }
-
- # Update the DNS record
- $url = "https://$config{$host}{'server'}/api2/admin/dns/edit";
- my $data = "domain=";
- $data .= $config{$key}{'login'};
- $data .= "&record_id=";
- $data .= $id;
- $data .= "&content=";
- $data .= $ip if $ip;
-
- $reply = geturl(opt('proxy'), $url, '', '', $headers, 'POST', $data);
- unless ($reply) {
- failed("updating %s: Could not connect to %s.", $host, $config{$host}{'server'});
- last;
- }
- last if !header_ok($host, $reply);
-
- # Strip header
- $reply =~ s/^.*?\n\n//s;
- $response = eval { decode_json($reply) };
- if ($response->{success} eq 'error') {
- failed ("%s", $response->{error});
- } else {
- success ("%s -- Updated Successfully to %s", $host, $ip);
- }
-
- # Cache
- $config{$host}{'ip'} = $ip;
- $config{$host}{'mtime'} = $now;
- $config{$host}{'status'} = 'good';
- }
- }
-}
-
-######################################################################
-## nic_duckdns_examples
-######################################################################
-sub nic_duckdns_examples {
- return < 'Bad authorization (username or password)',
- 'badsys' => 'The system parameter given was not valid',
-
- 'notfqdn' => 'A Fully-Qualified Domain Name was not provided',
- 'nohost' => 'The hostname specified does not exist in the database',
- '!yours' => 'The hostname specified exists, but not under the username currently being used',
- '!donator' => 'The offline setting was set, when the user is not a donator',
- '!active' => 'The hostname specified is in a Custom DNS domain which has not yet been activated.',
- 'abuse', => 'The hostname specified is blocked for abuse; you should receive an email notification ' .
- 'which provides an unblock request link. More info can be found on ' .
- 'https://www.dyndns.com/support/abuse.html',
-
- 'numhost' => 'System error: Too many or too few hosts found. Contact support@dyndns.org',
- 'dnserr' => 'System error: DNS error encountered. Contact support@dyndns.org',
-
- 'nochg' => 'No update required; unnecessary attempts to change to the current address are considered abusive',
- );
-
- my @hosts = @_;
- foreach my $key (keys @hosts) {
- my $h = $hosts[$key];
- my $ip = $config{$h}{'wantip'};
- delete $config{$h}{'wantip'};
-
- info("setting IP address to %s for %s", $ip, $h);
- verbose("UPDATE:","updating %s", $h);
-
- ## Select the DynDNS system to update
- my $url = "http://$config{$h}{'server'}$config{$h}{'script'}?system=";
- if ($config{$h}{'custom'}) {
- warning("updating %s: 'custom' and 'static' may not be used together. ('static' ignored)", $h)
- if $config{$h}{'static'};
- # warning("updating %s: 'custom' and 'offline' may not be used together. ('offline' ignored)", $h)
- # if $config{$h}{'offline'};
- $url .= 'custom';
-
- } elsif ($config{$h}{'static'}) {
- # warning("updating %s: 'static' and 'offline' may not be used together. ('offline' ignored)", $h)
- # if $config{$h}{'offline'};
- $url .= 'statdns';
-
- } else {
- $url .= 'dyndns';
- }
-
- $url .= "&hostname=$h";
- $url .= "&myip=";
- $url .= $ip if $ip;
-
- ## some args are not valid for a custom domain.
- $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');
- }
-
- my $reply = geturl(opt('proxy'), $url, $config{$h}{'login'}, $config{$h}{'password'});
- if (!defined($reply) || !$reply) {
- failed("updating %s: Could not connect to %s.", $h, $config{$h}{'server'});
- last;
- }
- last if !header_ok($h, $reply);
-
- my @reply = split /\n/, $reply;
- my $state = 'header';
- my $returnedip = $ip;
-
- foreach my $line (@reply) {
- if ($state eq 'header') {
- $state = 'body';
-
- } elsif ($state eq 'body') {
- $state = 'results' if $line eq '';
-
- } elsif ($state =~ /^results/) {
- $state = 'results2';
-
- # bug #10: some dyndns providers does not return the IP so
- # we can't use the returned IP
- my ($status, $returnedip) = split / /, lc $line;
- $ip = $returnedip if (not $ip);
- #my $h = shift @hosts;
-
- $config{$h}{'status'} = $status;
- if ($status eq 'good') {
- $config{$h}{'ip'} = $ip;
- $config{$h}{'mtime'} = $now;
- success("updating %s: %s: IP address set to %s", $h, $status, $ip);
-
- } elsif (exists $errors{$status}) {
- if ($status eq 'nochg') {
- warning("updating %s: %s: %s", $h, $status, $errors{$status});
- $config{$h}{'ip'} = $ip;
- $config{$h}{'mtime'} = $now;
- $config{$h}{'status'} = 'good';
-
- } else {
- failed("updating %s: %s: %s", $h, $status, $errors{$status});
- }
-
- } elsif ($status =~ /w(\d+)(.)/) {
- my ($wait, $units) = ($1, lc $2);
- my ($sec, $scale) = ($wait, 1);
-
- ($scale, $units) = (1, 'seconds') if $units eq 's';
- ($scale, $units) = (60, 'minutes') if $units eq 'm';
- ($scale, $units) = (60*60, 'hours') if $units eq 'h';
-
- $sec = $wait * $scale;
- $config{$h}{'wtime'} = $now + $sec;
- warning("updating %s: %s: wait $wait $units before further updates", $h, $status, $ip);
-
- } else {
- failed("updating %s: %s: unexpected status (%s)", $h, $line);
- }
- }
- }
- failed("updating %s: Could not connect to %s.", $h, $config{$h}{'server'})
- if $state ne 'results2';
- }
-}
-
-######################################################################
-## nic_dondominio_examples
-######################################################################
-sub nic_dondominio_examples {
- return < 'Invalid username or password, or invalid IP syntax',
- 'error-auth-suspend' => 'User has had their account suspended due to complaints or misuse of the service.',
- 'error-auth-voided' => 'User has had their account permanently revoked.',
- 'error-record-invalid' =>'Record ID number does not exist in the system.',
- 'error-record-auth' => 'User does not have access to this record.',
- 'error-record-ip-same' => 'No update required.',
- 'error-system' => 'General system error which is caught and recognized by the system.',
- 'error' => 'General system error unrecognized by the system.',
- 'success' => 'Record successfully updated!',
- );
-
- ## update each configured host
- ## should improve to update in one pass
- foreach my $h (@_) {
- my $ip = delete $config{$h}{'wantip'};
- info("Setting IP address to %s for %s", $ip, $h);
- verbose("UPDATE:","Updating %s", $h);
-
- # Set the URL that we're going to to update
- my $url;
- $url = $globals{'ssl'} ? "https://" : "http://";
- $url .= $config{$h}{'server'}.$config{$h}{'script'};
- $url .= "?username=$config{$h}{'login'}";
- $url .= "&password=$config{$h}{'password'}";
- $url .= "&ip=$ip";
- $url .= "&id=$h";
-
- # Try to get URL
- my $reply = geturl(opt('proxy'), $url);
-
- # No response, declare as failed
- if (!defined($reply) || !$reply) {
- failed("Updating %s: Could not connect to %s.", $h, $config{$h}{'server'});
- last;
- }
- last if !header_ok($h, $reply);
-
- my @reply = split /\n/, $reply;
- my $returned = pop(@reply);
- if ($returned =~ 'success')
- {
- $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';
- failed("Updating %s: Server said: '$returned': $messages{$returned}", $h);
- }
- }
-}
-
-######################################################################
-# vim: ai ts=4 sw=4 tw=78 :
-
-
-__END__
diff -Nru ddclient-3.9.1/ddclient.conf.in ddclient-3.10.0/ddclient.conf.in
--- ddclient-3.9.1/ddclient.conf.in 1970-01-01 00:00:00.000000000 +0000
+++ ddclient-3.10.0/ddclient.conf.in 2022-10-20 18:06:35.000000000 +0000
@@ -0,0 +1,317 @@
+######################################################################
+##
+## Define default global variables with lines like:
+## var=value [, var=value]*
+## These values will be used for each following host unless overridden
+## with a local variable definition.
+##
+## Define local variables for one or more hosts with:
+## var=value [, var=value]* host.and.domain[,host2.and.domain...]
+##
+## Lines can be continued on the following line by ending the line
+## with a \
+##
+##
+## Warning: not all supported routers or dynamic DNS services
+## are mentioned here.
+##
+######################################################################
+daemon=300 # check every 300 seconds
+syslog=yes # log update msgs to syslog
+mail=root # mail all msgs to root
+mail-failure=root # mail failed update msgs to root
+pid=@runstatedir@/ddclient.pid # record PID in file.
+ssl=yes # use ssl-support. Works with
+ # ssl-library
+# postscript=script # run script after updating. The
+ # new IP is added as argument.
+#
+#use=watchguard-soho, fw=192.168.111.1:80 # via Watchguard's SOHO FW
+#use=netopia-r910, fw=192.168.111.1:80 # via Netopia R910 FW
+#use=smc-barricade, fw=192.168.123.254:80 # via SMC's Barricade FW
+#use=netgear-rt3xx, fw=192.168.0.1:80 # via Netgear's internet FW
+#use=linksys, fw=192.168.1.1:80 # via Linksys's internet FW
+#use=maxgate-ugate3x00, fw=192.168.0.1:80 # via MaxGate's UGATE-3x00 FW
+#use=elsa-lancom-dsl10, fw=10.0.0.254:80 # via ELSA LanCom DSL/10 DSL Router
+#use=elsa-lancom-dsl10-ch01, fw=10.0.0.254:80 # via ELSA LanCom DSL/10 DSL Router
+#use=elsa-lancom-dsl10-ch02, fw=10.0.0.254:80 # via ELSA LanCom DSL/10 DSL Router
+#use=alcatel-stp, fw=10.0.0.138:80 # via Alcatel Speed Touch Pro
+#use=xsense-aero, fw=192.168.1.1:80 # via Xsense Aero Router
+#use=allnet-1298, fw=192.168.1.1:80 # via AllNet 1298 DSL Router
+#use=3com-oc-remote812, fw=192.168.0.254:80 # via 3com OfficeConnect Remote 812
+#use=e-tech, fw=192.168.1.1:80 # via E-tech Router
+#use=cayman-3220h, fw=192.168.0.1:1080 # via Cayman 3220-H DSL Router
+#
+#fw-login=admin, fw-password=XXXXXX # FW login and password
+#
+## To obtain an IP address from FW status page (using fw-login, fw-password)
+#use=fw, fw=192.168.1.254/status.htm, fw-skip='IP Address' # found after IP Address
+#
+## To obtain an IP address from Web status page (using the proxy if defined)
+## by default, checkip.dyndns.org is used if you use the dyndns protocol.
+## Using use=web is enough to get it working.
+## WARNING: set deamon at least to 600 seconds if you use checkip or you could
+## get banned from their service.
+#use=web, web=checkip.dyndns.org/, web-skip='IP Address' # found after IP Address
+#
+#use=ip, ip=127.0.0.1 # via static IP's
+#use=if, if=eth0 # via interfaces
+#use=web # via web
+#
+#protocol=dyndns2 # default protocol
+#proxy=fasthttp.sympatico.ca:80 # default proxy
+#server=members.dyndns.org # default server
+#server=members.dyndns.org:8245 # default server (bypassing proxies)
+
+#login=your-login # default login
+#password=test # default password
+#mx=mx.for.your.host # default MX
+#backupmx=yes|no # host is primary MX?
+#wildcard=yes|no # add wildcard CNAME?
+
+##
+## dyndns.org dynamic addresses
+##
+## (supports variables: wildcard,mx,backupmx)
+##
+# server=members.dyndns.org, \
+# protocol=dyndns2 \
+# your-dynamic-host.dyndns.org
+
+##
+## dyndns.org static addresses
+##
+## (supports variables: wildcard,mx,backupmx)
+##
+# static=yes, \
+# server=members.dyndns.org, \
+# protocol=dyndns2 \
+# your-static-host.dyndns.org
+
+##
+## dyndns.org custom addresses
+##
+## (supports variables: wildcard,mx,backupmx)
+##
+# custom=yes, \
+# server=members.dyndns.org, \
+# protocol=dyndns2 \
+# your-domain.top-level,your-other-domain.top-level
+
+##
+## ZoneEdit (zoneedit.com)
+##
+# server=dynamic.zoneedit.com, \
+# protocol=zoneedit1, \
+# login=your-zoneedit-login, \
+# password=your-zoneedit-password \
+# your.any.domain,your-2nd.any.dom
+
+##
+## EasyDNS (easydns.com)
+##
+# server=members.easydns.com, \
+# protocol=easydns, \
+# login=your-easydns-login, \
+# password=your-easydns-password \
+# your.any.domain,your-2nd.any.domain
+
+##
+## dslreports.com dynamic-host monitoring
+##
+# server=members.dslreports.com \
+# protocol=dslreports1, \
+# login=dslreports-login, \
+# password=dslreports-password \
+# dslreports-unique-id
+
+##
+## OrgDNS.org account-configuration
+##
+# use=web, web=members.orgdns.org/nic/ip
+# protocol=dyndns2
+# server=www.orgdns.org \
+# login=yourLoginName \
+# password=yourPassword \
+# yourSubdomain.orgdns.org
+
+##
+## NameCheap (namecheap.com)
+##
+# protocol=namecheap, \
+# server=dynamicdns.park-your-domain.com, \
+# login=example.com, \
+# password=example.com-password \
+# subdomain.example.com
+
+##
+## NearlyFreeSpeech.NET (nearlyfreespeech.net)
+##
+# protocol = nfsn, \
+# login=member-login, \
+# password=api-key, \
+# zone=example.com \
+# example.com,subdomain.example.com
+
+##
+## Loopia (loopia.se)
+##
+# use=web, web=loopia
+# protocol=dyndns2
+# server=dns.loopia.se
+# script=/XDynDNSServer/XDynDNS.php
+# login=my-loopia.se-login
+# password=my-loopia.se-password
+# my.domain.tld,other.domain.tld
+
+##
+## NoIP (noip.com)
+##
+# protocol=noip, \
+# ssl=yes, \
+# server=dynupdate.no-ip.com, \
+# login=your-noip-login, \
+# password=your-noip-password, \
+# your-host.domain.com, your-2nd-host.domain.com
+
+##
+## ChangeIP (changeip.com)
+##
+## single host update
+# protocol=changeip, \
+# login=my-my-changeip.com-login, \
+# password=my-changeip.com-password \
+# myhost.changeip.org
+
+##
+## CloudFlare (www.cloudflare.com)
+##
+#protocol=cloudflare, \
+#zone=domain.tld, \
+#ttl=1, \
+#login=your-login-email, \ # Only needed if you are using your global API key. If you are using an API token, set it to "token" (wihtout double quotes).
+#password=APIKey \ # This is either your global API key, or an API token. If you are using an API token, it must have the permissions "Zone - DNS - Edit" and "Zone - Zone - Read". The Zone resources must be "Include - All zones".
+#domain.tld,my.domain.tld
+
+##
+## Gandi (gandi.net)
+##
+## Single host update
+# protocol=gandi, \
+# zone=example.com, \
+# password=my-gandi-api-key, \
+# ttl=3h \
+# myhost.example.com
+
+##
+## Google Domains (www.google.com/domains)
+##
+# protocol=googledomains,
+# login=my-auto-generated-username,
+# password=my-auto-generated-password
+# my.domain.tld, otherhost.domain.tld
+
+##
+## Duckdns (http://www.duckdns.org/)
+##
+#
+# protocol=duckdns, \
+# password=my-auto-generated-password \
+# hostwithoutduckdnsorg
+
+##
+## Freemyip (http://freemyip.com/)
+##
+#
+# protocol=freemyip,
+# password=my-token
+# myhost
+
+##
+## MyOnlinePortal (http://myonlineportal.net)
+##
+# # ipv6=yes # optional
+# use=web, web=myonlineportal.net/checkip
+# # use=if, if=eth0 # alternative to use=web
+# # if-skip=Scope:Link # alternative to use=web
+# protocol=dyndns2
+# ssl=yes
+# login=your-myonlineportal-username
+# password=your-myonlineportal-password
+# domain.myonlineportal.net
+
+##
+## nsupdate.info IPV4(https://www.nsupdate.info)
+##
+#use=web, web=http://ipv4.nsupdate.info/myip
+#protocol=dyndns2
+#server=ipv4.nsupdate.info
+#login=domain.nsupdate.info
+#password='123'
+#domain.nsupdate.info
+
+##
+## nsupdate.info IPV6 (https://www.nsupdate.info)
+## ddclient releases <= 3.8.1 do not support IPv6
+##
+#usev6=if, if=eth0
+#protocol=dyndns2
+#server=ipv6.nsupdate.info
+#login=domain.nsupdate.info
+#password='123'
+#domain.nsupdate.info
+
+##
+## Yandex.Mail for Domain (domain.yandex.com)
+##
+# protocol=yandex, \
+# login=domain.tld, \
+# password=yandex-pdd-token \
+# my.domain.tld,other.domain.tld \
+
+##
+## DNS Made Easy (https://dnsmadeeasy.com)
+##
+# protocol=dnsmadeeasy,
+# login=your-account-email-address
+# password=your-generated-password
+# your-numeric-record-id-1,your-numeric-record-id-2,...
+
+##
+## OVH DynHost (https://ovh.com)
+##
+# protocol=ovh,
+# login=example.com-dynhostuser,
+# password=your_password
+# test.example.com
+
+##
+## ClouDNS (https://www.cloudns.net)
+##
+# protocol=cloudns, \
+# dynurl=https://ipv4.cloudns.net/api/dynamicURL/?q=Njc1OTE2OjY3Njk0NDM6YTk2, \
+# myhost.example.com
+
+##
+## dinahosting (https://dinahosting.com)
+##
+# protocol=dinahosting, \
+# login=myusername, \
+# password=mypassword \
+# myhost.mydomain.com
+
+##
+## dnsexit (www.dnsexit.com)
+##
+#protocol=dnsexit, \
+#login=myusername, \
+#password=mypassword, \
+#subdomain-1.domain.com,subdomain-2.domain.com
+
+##
+## Njal.la (http://njal.la/)
+##
+# protocol=njalla,
+# password=mypassword
+# quietreply=no|yes
+# my-domain.com
diff -Nru ddclient-3.9.1/ddclient.in ddclient-3.10.0/ddclient.in
--- ddclient-3.9.1/ddclient.in 1970-01-01 00:00:00.000000000 +0000
+++ ddclient-3.10.0/ddclient.in 2022-10-20 18:06:35.000000000 +0000
@@ -0,0 +1,7185 @@
+#!/usr/bin/perl
+######################################################################
+#
+# DDCLIENT - a Perl client for updating DynDNS information
+#
+# Author: Paul Burry (paul+ddclient@burry.ca)
+# ddclient developers: see https://github.com/orgs/ddclient/people
+#
+# website: https://ddclient.net
+#
+# Support for multiple IP numbers added by
+# Astaro AG, Ingo Schwarze September 16, 2008
+#
+# Support for multiple domain support for Namecheap by Robert Ian Hawdon 2010-09-03: https://robertianhawdon.me.uk/
+#
+# Initial Cloudflare support by Ian Pye, updated by Robert Ian Hawdon 2012-07-16
+# Further updates by Peter Roberts to support the new API 2013-09-26, 2014-06-22: http://blog.peter-r.co.uk/
+#
+#
+######################################################################
+package ddclient;
+require v5.10.1;
+use strict;
+use warnings;
+use File::Basename;
+use File::Path qw(make_path);
+use File::Temp;
+use Getopt::Long;
+use IO::Socket::INET;
+use Socket qw(AF_INET AF_INET6 PF_INET PF_INET6);
+use Sys::Hostname;
+
+use version 0.77; our $VERSION = version->declare('v3.10.0');
+(my $version = $VERSION->stringify()) =~ s/^v//;
+my $programd = $0;
+$programd =~ s%^.*/%%;
+my $program = $programd;
+$program =~ s/d$//;
+my $now = time;
+my $hostname = hostname();
+
+# subst_var(subst, default) returns subst unless it looks like @foo@ in which case it returns
+# default. The @foo@ strings are expected to be replaced by make; this function makes it possible
+# to run this file as a Perl script before those substitutions are made.
+sub subst_var {
+ my ($subst, $default) = @_;
+ return $default if $subst =~ qr'^@\w+@$';
+ return $subst;
+}
+
+my $etc = subst_var('@sysconfdir@', '/etc/ddclient');
+my $cachedir = subst_var('@localstatedir@', '/var') . '/cache/ddclient';
+my $savedir = '/tmp';
+if ($program =~ /test/i) {
+ $etc = '.';
+ $cachedir = '.';
+ $savedir = 'URL';
+}
+
+my $msgs = '';
+my $last_msgs = '';
+
+## If run as *d (e.g., ddclientd) then daemonize by default (but allow
+## flags and options to override).
+my $daemon_default = ($programd =~ /d$/) ? interval('5m') : 0;
+
+use vars qw($file $lineno);
+local $file = '';
+local $lineno = '';
+
+$ENV{'PATH'} = (exists($ENV{PATH}) ? "$ENV{PATH}:" : "") . "/sbin:/usr/sbin:/bin:/usr/bin:/etc:/usr/lib:";
+
+our %globals;
+my ($result, %config, %cache);
+my $saved_cache;
+my %saved_opt;
+my $daemon;
+# Control how many times warning message logged for invalid IP addresses
+my (%warned_ip, %warned_ipv4, %warned_ipv6);
+my $inv_ip_warn_count = opt('max-warn') // 1;
+
+sub T_ANY { 'any' }
+sub T_STRING { 'string' }
+sub T_EMAIL { 'e-mail address' }
+sub T_NUMBER { 'number' }
+sub T_DELAY { 'time delay (ie. 1d, 1hour, 1m)' }
+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' }
+sub T_USE { 'ip strategy' }
+sub T_USEV4 { 'ipv4 strategy' }
+sub T_USEV6 { 'ipv6 strategy' }
+sub T_IF { 'interface' }
+sub T_PROG { 'program name' }
+sub T_IP { 'ip' }
+sub T_IPV4 { 'ipv4' }
+sub T_IPV6 { 'ipv6' }
+sub T_POSTS { 'postscript' }
+
+## strategies for obtaining an ip address.
+my %builtinweb = (
+ 'dyndns' => {'url' => 'http://checkip.dyndns.org/', 'skip' => 'Current IP Address:'},
+ 'freedns' => {'url' => 'https://freedns.afraid.org/dynamic/check.php'},
+ 'googledomains' => {'url' => 'https://domains.google.com/checkip'},
+ 'he' => {'url' => 'https://checkip.dns.he.net/'},
+ 'ip4only.me' => {'url' => 'https://ip4only.me/api/'},
+ 'ip6only.me' => {'url' => 'https://ip6only.me/api/'},
+ 'ipify-ipv4' => {'url' => 'https://api.ipify.org/'},
+ 'ipify-ipv6' => {'url' => 'https://api6.ipify.org/'},
+ 'loopia' => {'url' => 'https://dns.loopia.se/checkip/checkip.php', 'skip' => 'Current IP Address:'},
+ 'myonlineportal' => {'url' => 'https://myonlineportal.net/checkip'},
+ 'noip-ipv4' => {'url' => 'http://ip1.dynupdate.no-ip.com/'},
+ 'noip-ipv6' => {'url' => 'http://ip1.dynupdate6.no-ip.com/'},
+ 'nsupdate.info-ipv4' => {'url' => 'http://ipv4.nsupdate.info/myip'},
+ 'nsupdate.info-ipv6' => {'url' => 'http://ipv6.nsupdate.info/myip'},
+ 'zoneedit' => {'url' => 'https://dynamic.zoneedit.com/checkip.html'},
+);
+my %builtinfw = (
+ '2wire' => {
+ 'name' => '2Wire 1701HG Gateway',
+ 'url' => '/xslt?PAGE=B01',
+ 'skip' => 'Internet Address:',
+ },
+ '3com-3c886a' => {
+ 'name' => '3com 3c886a 56k Lan Modem',
+ 'url' => '/stat3.htm',
+ 'skip' => 'IP address in use',
+ },
+ '3com-oc-remote812' => {
+ 'name' => '3com OfficeConnect Remote 812',
+ 'url' => '/callEvent',
+ 'skip' => '.*LOCAL',
+ },
+ 'alcatel-510' => {
+ 'name' => 'Alcatel Speed Touch 510',
+ 'url' => '/cgi/ip/',
+ 'skip' => 'ppp',
+ },
+ 'alcatel-530' => {
+ 'name' => 'Alcatel/Thomson SpeedTouch 530',
+ 'url' => '/cgi/status/',
+ 'skip' => 'IP Address',
+ },
+ 'alcatel-stp' => {
+ 'name' => 'Alcatel Speed Touch Pro',
+ 'url' => '/cgi/router/',
+ 'skip' => 'Brt',
+ },
+ 'allnet-1298' => {
+ 'name' => 'Allnet 1298',
+ 'url' => '/cgi/router/',
+ 'skip' => 'WAN',
+ },
+ 'cayman-3220h' => {
+ 'name' => 'Cayman 3220-H DSL',
+ 'url' => '/shell/show+ip+interfaces',
+ 'skip' => '.*inet',
+ },
+ 'dlink-524' => {
+ 'name' => 'D-Link DI-524',
+ 'url' => '/st_device.html',
+ 'skip' => 'WAN.*?Addres',
+ },
+ 'dlink-604' => {
+ 'name' => 'D-Link DI-604',
+ 'url' => '/st_devic.html',
+ 'skip' => 'WAN.*?IP.*Address',
+ },
+ 'dlink-614' => {
+ 'name' => 'D-Link DI-614+',
+ 'url' => '/st_devic.html',
+ 'skip' => 'WAN',
+ },
+ 'e-tech' => {
+ 'name' => 'E-tech Router',
+ 'url' => '/Status.htm',
+ 'skip' => 'Public IP Address',
+ },
+ 'elsa-lancom-dsl10' => {
+ 'name' => 'ELSA LanCom DSL/10 DSL FW',
+ 'url' => '/config/1/6/8/3/',
+ 'skip' => 'IP.Address',
+ },
+ 'elsa-lancom-dsl10-ch01' => {
+ 'name' => 'ELSA LanCom DSL/10 DSL FW (isdn ch01)',
+ 'url' => '/config/1/6/8/3/',
+ 'skip' => 'IP.Address.*?CH01',
+ },
+ 'elsa-lancom-dsl10-ch02' => {
+ 'name' => 'ELSA LanCom DSL/10 DSL FW (isdn ch01)',
+ 'url' => '/config/1/6/8/3/',
+ 'skip' => 'IP.Address.*?CH02',
+ },
+ 'linksys' => {
+ 'name' => 'Linksys FW',
+ 'url' => '/Status.htm',
+ 'skip' => 'WAN.*?Address',
+ },
+ 'linksys-rv042-wan1' => {
+ 'name' => 'Linksys RV042 Dual Homed Router WAN Port 2',
+ 'url' => '/home.htm',
+ 'skip' => 'WAN1 IP',
+ },
+ 'linksys-rv042-wan2' => {
+ 'name' => 'Linksys RV042 Dual Homed Router WAN Port 2',
+ 'url' => '/home.htm',
+ 'skip' => 'WAN2 IP',
+ },
+ 'linksys-ver2' => {
+ 'name' => 'Linksys FW version 2',
+ 'url' => '/RouterStatus.htm',
+ 'skip' => 'WAN.*?Address',
+ },
+ 'linksys-ver3' => {
+ 'name' => 'Linksys FW version 3',
+ 'url' => '/Status_Router.htm',
+ 'skip' => 'WAN.*?Address',
+ },
+ 'linksys-wcg200' => {
+ 'name' => 'Linksys WCG200 FW',
+ 'url' => '/RgStatus.asp',
+ 'skip' => 'WAN.IP.*?Address',
+ },
+ 'linksys-wrt854g' => {
+ 'name' => 'Linksys WRT854G FW',
+ 'url' => '/Status_Router.asp',
+ 'skip' => 'IP Address:',
+ },
+ 'maxgate-ugate3x00' => {
+ 'name' => 'MaxGate UGATE-3x00 FW',
+ 'url' => '/Status.htm',
+ 'skip' => 'WAN.*?IP Address',
+ },
+ 'netcomm-nb3' => {
+ 'name' => 'NetComm NB3',
+ 'url' => '/MainPage?id=6',
+ 'skip' => 'ppp-0',
+ },
+ 'netgear-dg834g' => {
+ 'name' => 'netgear-dg834g',
+ 'url' => '/setup.cgi?next_file=s_status.htm&todo=cfg_init',
+ 'skip' => '',
+ },
+ 'netgear-rp614' => {
+ 'name' => 'Netgear RP614 FW',
+ 'url' => '/sysstatus.html',
+ 'skip' => 'IP Address',
+ },
+ 'netgear-rt3xx' => {
+ 'name' => 'Netgear FW',
+ 'url' => '/mtenSysStatus.html',
+ 'skip' => 'IP Address',
+ },
+ 'netgear-wgt624' => {
+ 'name' => 'Netgear WGT624',
+ 'url' => '/RST_st_dhcp.htm',
+ 'skip' => 'IP Address | ',
+ },
+ 'netgear-wpn824' => {
+ 'name' => 'Netgear WPN824 FW',
+ 'url' => '/RST_status.htm',
+ 'skip' => 'IP Address',
+ },
+ 'netopia-r910' => {
+ 'name' => 'Netopia R910 FW',
+ 'url' => '/WanEvtLog',
+ 'skip' => 'local:',
+ },
+ 'olitec-SX200' => {
+ 'name' => 'olitec-SX200',
+ 'url' => '/doc/wan.htm',
+ 'skip' => 'st_wan_ip[0] = "',
+ },
+ 'rtp300' => {
+ 'name' => 'Linksys RTP300',
+ 'url' => '/cgi-bin/webcm?getpage=%2Fusr%2Fwww_safe%2Fhtml%2Fstatus%2FRouter.html',
+ 'skip' => 'Internet.*?IP Address',
+ },
+ 'siemens-ss4200' => {
+ 'name' => 'Siemens SpeedStream 4200',
+ 'url' => '/summary.htm',
+ 'skip' => '',
+ },
+ 'sitecom-dc202' => {
+ 'name' => 'Sitecom DC-202 FW',
+ 'url' => '/status.htm',
+ 'skip' => 'Internet IP Address',
+ },
+ 'smc-barricade' => {
+ 'name' => 'SMC Barricade FW',
+ 'url' => '/status.htm',
+ 'skip' => 'IP Address',
+ },
+ 'smc-barricade-7004vbr' => {
+ 'name' => 'SMC Barricade FW (7004VBR model config)',
+ 'url' => '/status_main.stm',
+ 'skip' => 'var wan_ip=',
+ },
+ 'smc-barricade-7401bra' => {
+ 'name' => 'SMC Barricade 7401BRA FW',
+ 'url' => '/admin/wan1.htm',
+ 'skip' => 'IP Address',
+ },
+ 'smc-barricade-alt' => {
+ 'name' => 'SMC Barricade FW (alternate config)',
+ 'url' => '/status.HTM',
+ 'skip' => 'WAN IP',
+ },
+ 'sohoware-nbg800' => {
+ 'name' => 'SOHOWare BroadGuard NBG800',
+ 'url' => '/status.htm',
+ 'skip' => 'Internet IP',
+ },
+ 'sveasoft' => {
+ 'name' => 'Sveasoft WRT54G/WRT54GS',
+ 'url' => '/Status_Router.asp',
+ 'skip' => 'var wan_ip',
+ },
+ 'thomson-st536v6' => {
+ 'name' => 'Thomson SpeedTouch 536v6',
+ 'url' => '/cgi/b/is/',
+ 'skip' => 'IP Address',
+ },
+ 'thomson-tg782' => {
+ 'name' => 'Thomson/Technicolor TG782',
+ 'url' => '/cgi/b/is/',
+ 'skip' => 'IP Address',
+ },
+ 'vigor-2200usb' => {
+ 'name' => 'Vigor 2200 USB',
+ 'url' => '/doc/online.sht',
+ 'skip' => 'PPPoA',
+ },
+ 'watchguard-edge-x' => {
+ 'name' => 'Watchguard Edge X FW',
+ 'url' => '/netstat.htm',
+ 'skip' => 'inet addr:',
+ },
+ 'watchguard-soho' => {
+ 'name' => 'Watchguard SOHO FW',
+ 'url' => '/pubnet.htm',
+ 'skip' => 'NAME=IPAddress VALUE=',
+ },
+ 'westell-6100' => {
+ 'name' => 'Westell C90-610015-06 DSL Router',
+ 'url' => '/advstat.htm',
+ 'skip' => 'IP.+?Address',
+ },
+ 'xsense-aero' => {
+ 'name' => 'Xsense Aero',
+ 'url' => '/A_SysInfo.htm',
+ 'skip' => 'WAN.*?IP Address',
+ },
+);
+
+my %ip_strategies = (
+ 'no' => ": deprecated, see 'usev4' and 'usev6'",
+ 'ip' => ": deprecated, see 'usev4' and 'usev6'",
+ 'web' => ": deprecated, see 'usev4' and 'usev6'",
+ 'fw' => ": deprecated, see 'usev4' and 'usev6'",
+ 'if' => ": deprecated, see 'usev4' and 'usev6'",
+ 'cmd' => ": deprecated, see 'usev4' and 'usev6'",
+ 'cisco' => ": deprecated, see 'usev4' and 'usev6'",
+ 'cisco-asa' => ": deprecated, see 'usev4' and 'usev6'",
+ map({ $_ => sprintf(": Built-in firewall %s deprecated, see 'usev4' and 'usev6'",
+ $builtinfw{$_}->{'name'}) }
+ keys(%builtinfw)),
+);
+
+sub ip_strategies_usage {
+ return map({ sprintf(" -use=%-22s %s.", $_, $ip_strategies{$_}) }
+ ('ip', 'web', 'if', 'cmd', 'fw', sort('cisco', 'cisco-asa', keys(%builtinfw))));
+}
+
+my %ipv4_strategies = (
+ 'disabled' => ": do not obtain an IPv4 address for this host",
+ 'ipv4' => ": obtain IPv4 from -ipv4 {address}",
+ 'webv4' => ": obtain IPv4 from an IP discovery page on the web",
+ 'ifv4' => ": obtain IPv4 from the -ifv4 {interface}",
+ 'cmdv4' => ": obtain IPv4 from the -cmdv4 {external-command}",
+ 'fwv4' => ": obtain IPv4 from the firewall specified by -fwv4 {type|address}",
+ 'ciscov4' => ": obtain IPv4 from Cisco FW at the -fwv4 {address}",
+ 'cisco-asav4' => ": obtain IPv4 from Cisco ASA at the -fwv4 {address}",
+ map { $_ => sprintf ": obtain IPv4 from %s at the -fwv4 {address}", $builtinfw{$_}->{'name'} } keys %builtinfw,
+);
+sub ipv4_strategies_usage {
+ return map { sprintf(" -usev4=%-22s %s.", $_, $ipv4_strategies{$_}) } sort keys %ipv4_strategies;
+}
+
+my %ipv6_strategies = (
+ 'no' => ": deprecated, use 'disabled'",
+ 'disabled' => ": do not obtain an IPv6 address for this host",
+ 'ip' => ": deprecated, use 'ipv6'",
+ 'ipv6' => ": obtain IPv6 from -ipv6 {address}",
+ 'web' => ": deprecated, use 'webv6'",
+ 'webv6' => ": obtain IPv6 from an IP discovery page on the web",
+ 'if' => ": deprecated, use 'ifv6'",
+ 'ifv6' => ": obtain IPv6 from the -if {interface}",
+ 'cmd' => ": deprecated, use 'cmdv6'",
+ 'cmdv6' => ": obtain IPv6 from the -cmdv6 {external-command}",
+ 'fwv6' => ": obtain IPv6 from the firewall specified by -fwv6 {type|address}",
+ 'ciscov6' => ": obtain IPv6 from Cisco FW at the -fwv6 {address}",
+ 'cisco-asav6' => ": obtain IPv6 from Cisco ASA at the -fwv6 {address}",
+ map { $_ => sprintf ": obtain IPv6 from %s at the -fwv6 {address}", $builtinfw{$_}->{'name'} } keys %builtinfw,
+);
+sub ipv6_strategies_usage {
+ return map { sprintf(" -usev6=%-22s %s.", $_, $ipv6_strategies{$_}) } sort keys %ipv6_strategies;
+}
+
+sub setv {
+ return {
+ 'type' => shift,
+ 'required' => shift,
+ 'cache' => shift,
+ 'default' => shift,
+ 'minimum' => shift,
+ };
+}
+my %variables = (
+ 'global-defaults' => {
+ 'daemon' => setv(T_DELAY, 0, 0, $daemon_default, interval('60s')),
+ 'foreground' => setv(T_BOOL, 0, 0, 0, undef),
+ 'file' => setv(T_FILE, 0, 0, "$etc/$program.conf", undef),
+ 'cache' => setv(T_FILE, 0, 0, "$cachedir/$program.cache", undef),
+ 'pid' => setv(T_FILE, 0, 0, "", undef),
+ 'proxy' => setv(T_FQDNP, 0, 0, undef, undef),
+ 'protocol' => setv(T_PROTO, 0, 0, 'dyndns2', undef),
+
+ 'use' => setv(T_USE, 0, 0, 'ip', undef),
+ 'usev4' => setv(T_USEV4, 0, 0, 'disabled', undef),
+ 'usev6' => setv(T_USEV6, 0, 0, 'disabled', undef),
+ 'ip' => setv(T_IP, 0, 0, undef, undef),
+ 'ipv4' => setv(T_IPV4, 0, 0, undef, undef),
+ 'ipv6' => setv(T_IPV6, 0, 0, undef, undef),
+ 'if' => setv(T_IF, 0, 0, 'ppp0', undef),
+ 'ifv4' => setv(T_IF, 0, 0, 'default', undef),
+ 'ifv6' => setv(T_IF, 0, 0, 'default', undef),
+ 'web' => setv(T_STRING,0, 0, 'dyndns', undef),
+ 'web-skip' => setv(T_STRING,1, 0, '', undef),
+ 'webv4' => setv(T_STRING,0, 0, 'googledomains', undef),
+ 'webv4-skip' => setv(T_STRING,1, 0, '', undef),
+ 'webv6' => setv(T_STRING,0, 0, 'googledomains', undef),
+ 'webv6-skip' => setv(T_STRING,1, 0, '', undef),
+ 'fw' => setv(T_ANY, 0, 0, '', undef),
+ 'fw-skip' => setv(T_STRING,1, 0, '', undef),
+ 'fwv4' => setv(T_ANY, 0, 0, '', undef),
+ 'fwv4-skip' => setv(T_STRING,1, 0, '', undef),
+ 'fwv6' => setv(T_ANY, 0, 0, '', undef),
+ 'fwv6-skip' => setv(T_STRING,1, 0, '', undef),
+ 'fw-login' => setv(T_LOGIN, 1, 0, '', undef),
+ 'fw-password' => setv(T_PASSWD,1, 0, '', undef),
+ 'cmd' => setv(T_PROG, 0, 0, '', undef),
+ 'cmd-skip' => setv(T_STRING,1, 0, '', undef),
+ 'cmdv4' => setv(T_PROG, 0, 0, '', undef),
+ 'cmdv6' => setv(T_PROG, 0, 0, '', undef),
+
+ 'timeout' => setv(T_DELAY, 0, 0, interval('120s'), interval('120s')),
+ 'retry' => setv(T_BOOL, 0, 0, 0, undef),
+ 'force' => setv(T_BOOL, 0, 0, 0, undef),
+ 'ssl' => setv(T_BOOL, 0, 0, 0, undef),
+ 'curl' => setv(T_BOOL, 0, 0, 0, undef),
+ 'syslog' => setv(T_BOOL, 0, 0, 0, undef),
+ 'facility' => setv(T_STRING,0, 0, 'daemon', undef),
+ 'priority' => setv(T_STRING,0, 0, 'notice', undef),
+ 'mail' => setv(T_EMAIL, 0, 0, '', undef),
+ 'mail-failure' => setv(T_EMAIL, 0, 0, '', undef),
+ 'max-warn' => setv(T_NUMBER,0, 0, 1, undef),
+
+ 'exec' => setv(T_BOOL, 0, 0, 1, undef),
+ '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),
+ 'geturl' => setv(T_STRING,0, 0, '', undef),
+
+ 'postscript' => setv(T_POSTS, 0, 0, '', undef),
+ 'ssl_ca_dir' => setv(T_FILE, 0, 0, undef, undef),
+ 'ssl_ca_file' => setv(T_FILE, 0, 0, undef, undef),
+ },
+ 'service-common-defaults' => {
+ 'server' => setv(T_FQDNP, 1, 0, 'members.dyndns.org', undef),
+ 'login' => setv(T_LOGIN, 1, 0, '', undef),
+ 'password' => setv(T_PASSWD,1, 0, '', undef),
+ 'host' => setv(T_STRING,1, 1, '', undef),
+
+ 'use' => setv(T_USE, 0, 0, 'ip', undef),
+ 'if' => setv(T_IF, 0, 0, 'ppp0', undef),
+ 'web' => setv(T_STRING,0, 0, 'dyndns', undef),
+ 'web-skip' => setv(T_STRING,0, 0, '', undef),
+ 'web-ssl-validate' => setv(T_BOOL, 0, 0, 1, undef),
+ 'fw' => setv(T_ANY, 0, 0, '', undef),
+ 'fw-skip' => setv(T_STRING,0, 0, '', undef),
+ 'fw-login' => setv(T_LOGIN, 0, 0, '', undef),
+ 'fw-password' => setv(T_PASSWD,0, 0, '', undef),
+ 'fw-ssl-validate' => setv(T_BOOL, 0, 0, 1, undef),
+ 'cmd' => setv(T_PROG, 0, 0, '', undef),
+ 'cmd-skip' => setv(T_STRING,0, 0, '', undef),
+ 'ip' => setv(T_IP, 0, 1, undef, undef), #TODO remove from cache?
+ 'ipv4' => setv(T_IPV4, 0, 1, undef, undef),
+ 'ipv6' => setv(T_IPV6, 0, 1, undef, undef),
+ 'wtime' => setv(T_DELAY, 0, 1, 0, interval('30s')),
+ 'mtime' => setv(T_NUMBER,0, 1, 0, undef),
+ 'atime' => setv(T_NUMBER,0, 1, 0, undef),
+ 'status' => setv(T_ANY, 0, 1, '', undef), #TODO remove from cache?
+ 'status-ipv4' => setv(T_ANY, 0, 1, '', undef),
+ 'status-ipv6' => setv(T_ANY, 0, 1, '', undef),
+ 'min-interval' => setv(T_DELAY, 0, 0, interval('30s'), 0),
+ 'max-interval' => setv(T_DELAY, 0, 0, interval('25d'), 0),
+ 'min-error-interval' => setv(T_DELAY, 0, 0, interval('5m'), 0),
+
+ 'warned-min-interval' => setv(T_ANY, 0, 1, 0, undef),
+ 'warned-min-error-interval' => setv(T_ANY, 0, 1, 0, undef),
+ },
+ 'dyndns-common-defaults' => {
+ 'backupmx' => setv(T_BOOL, 0, 1, 0, undef),
+ 'mx' => setv(T_OFQDN, 0, 1, '', undef),
+ 'static' => setv(T_BOOL, 0, 1, 0, undef),
+ 'wildcard' => setv(T_BOOL, 0, 1, 0, undef),
+ },
+ 'keysystems-common-defaults' => {
+ 'server' => setv(T_FQDNP, 1, 0, 1, 'dynamicdns.key-systems.net', undef),
+ 'login' => setv(T_LOGIN, 0, 0, 0, 'unused', undef),
+ },
+ 'dnsexit-common-defaults' => {
+ 'ssl' => setv(T_BOOL, 0, 0, 1, undef),
+ 'server' => setv(T_FQDNP, 1, 0, 'update.dnsexit.com', undef),
+ 'script' => setv(T_STRING, 0, 1, '/RemoteUpdate.sv', undef),
+ 'min-error-interval' => setv(T_DELAY, 0, 0, interval('8m'), 0),
+ },
+);
+my %services = (
+ '1984' => {
+ 'updateable' => undef,
+ 'update' => \&nic_1984_update,
+ 'examples' => \&nic_1984_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'login' => setv(T_LOGIN, 0, 0, 'unused', undef),
+ 'server' => setv(T_FQDNP, 1, 0, 'api.1984.is', undef),
+ },
+ },
+ 'changeip' => {
+ 'updateable' => undef,
+ 'update' => \&nic_changeip_update,
+ 'examples' => \&nic_changeip_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'min-interval' => setv(T_DELAY, 0, 0, 0, interval('5m')),
+ 'server' => setv(T_FQDNP, 1, 0, 'nic.changeip.com', undef),
+ },
+ },
+ 'cloudflare' => {
+ 'updateable' => undef,
+ 'update' => \&nic_cloudflare_update,
+ 'examples' => \&nic_cloudflare_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ '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),
+ 'server' => setv(T_FQDNP, 1, 0, 'api.cloudflare.com/client/v4', undef),
+ 'static' => setv(T_BOOL, 0, 1, 0, undef),
+ 'ttl' => setv(T_NUMBER, 1, 0, 1, undef),
+ 'wildcard' => setv(T_BOOL, 0, 1, 0, undef),
+ 'zone' => setv(T_FQDN, 1, 0, '', undef),
+ },
+ },
+ 'cloudns' => {
+ 'updateable' => undef,
+ 'update' => \&nic_cloudns_update,
+ 'examples' => \&nic_cloudns_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'dynurl' => setv(T_STRING, 1, 0, undef, undef),
+ # nic_updateable() assumes that every service uses a username and password but that is
+ # not true for CloudNS. Silence warnings by redefining the username and password
+ # variables as non-required with a non-empty default.
+ 'login' => setv(T_STRING, 0, 0, 'unused', undef),
+ 'password' => setv(T_STRING, 0, 0, 'unused', undef),
+ },
+ },
+ 'dinahosting' => {
+ 'updateable' => undef,
+ 'update' => \&nic_dinahosting_update,
+ 'examples' => \&nic_dinahosting_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'min-error-interval' => setv(T_DELAY, 0, 0, interval('8m'), 0),
+ 'script' => setv(T_STRING, 0, 1, '/special/api.php', undef),
+ 'server' => setv(T_FQDNP, 1, 0, 'dinahosting.com', undef),
+ },
+ },
+ 'dnsmadeeasy' => {
+ 'updateable' => undef,
+ 'update' => \&nic_dnsmadeeasy_update,
+ 'examples' => \&nic_dnsmadeeasy_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'script' => setv(T_STRING, 1, 1, '/servlet/updateip', undef),
+ 'server' => setv(T_FQDNP, 1, 0, 'cp.dnsmadeeasy.com', undef),
+ },
+ },
+ 'dondominio' => {
+ 'updateable' => undef,
+ 'update' => \&nic_dondominio_update,
+ 'examples' => \&nic_dondominio_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'server' => setv(T_FQDNP, 1, 0, 'dondns.dondominio.com', undef),
+ },
+ },
+ 'dslreports1' => {
+ 'updateable' => undef,
+ 'update' => \&nic_dslreports1_update,
+ 'examples' => \&nic_dslreports1_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'server' => setv(T_FQDNP, 1, 0, 'www.dslreports.com', undef),
+ 'host' => setv(T_NUMBER, 1, 1, 0, undef),
+ },
+ },
+ 'duckdns' => {
+ 'updateable' => undef,
+ 'update' => \&nic_duckdns_update,
+ 'examples' => \&nic_duckdns_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'login' => setv(T_LOGIN, 0, 0, 'unused', undef),
+ 'server' => setv(T_FQDNP, 1, 0, 'www.duckdns.org', undef),
+ },
+ },
+ 'dyndns1' => {
+ 'updateable' => \&nic_dyndns2_updateable,
+ 'update' => \&nic_dyndns1_update,
+ 'examples' => \&nic_dyndns1_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ %{$variables{'dyndns-common-defaults'}},
+ },
+ },
+ 'dyndns2' => {
+ 'updateable' => \&nic_dyndns2_updateable,
+ 'update' => \&nic_dyndns2_update,
+ 'examples' => \&nic_dyndns2_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ %{$variables{'dyndns-common-defaults'}},
+ 'custom' => setv(T_BOOL, 0, 1, 0, undef),
+ 'script' => setv(T_STRING, 1, 1, '/nic/update', undef),
+ },
+ },
+ 'easydns' => {
+ 'updateable' => undef,
+ 'update' => \&nic_easydns_update,
+ 'examples' => \&nic_easydns_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'backupmx' => setv(T_BOOL, 0, 1, 0, undef),
+ 'min-interval' => setv(T_DELAY, 0, 0, interval('5m'), 0),
+ 'mx' => setv(T_OFQDN, 0, 1, '', undef),
+ 'server' => setv(T_FQDNP, 1, 0, 'api.cp.easydns.com', undef),
+ 'script' => setv(T_STRING, 1, 1, '/dyn/generic.php', undef),
+ 'wildcard' => setv(T_BOOL, 0, 1, 0, undef),
+ },
+ },
+ 'freedns' => {
+ 'updateable' => undef,
+ 'update' => \&nic_freedns_update,
+ 'examples' => \&nic_freedns_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'min-interval' => setv(T_DELAY, 0, 0, 0, interval('5m')),
+ 'server' => setv(T_FQDNP, 1, 0, 'freedns.afraid.org', undef),
+ },
+ },
+ 'freemyip' => {
+ 'updateable' => undef,
+ 'update' => \&nic_freemyip_update,
+ 'examples' => \&nic_freemyip_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'login' => setv(T_LOGIN, 0, 0, 'unused', undef),
+ 'server' => setv(T_FQDNP, 1, 0, 'freemyip.com', undef),
+ },
+ },
+ 'gandi' => {
+ 'updateable' => undef,
+ 'update' => \&nic_gandi_update,
+ 'examples' => \&nic_gandi_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'min-interval' => setv(T_DELAY, 0, 0, 0, interval('5m')),
+ 'server' => setv(T_FQDNP, 1, 0, 'api.gandi.net', undef),
+ 'script' => setv(T_STRING, 1, 1, '/v5', undef),
+ 'ttl' => setv(T_DELAY, 0, 0, undef, interval('5m')),
+ 'zone' => setv(T_FQDN, 1, 0, undef, undef),
+ # Unused variables.
+ 'login' => setv(T_STRING, 0, 0, 'unused', undef),
+ }
+ },
+ 'godaddy' => {
+ 'updateable' => undef,
+ 'update' => \&nic_godaddy_update,
+ 'examples' => \&nic_godaddy_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'min-interval' => setv(T_DELAY, 0, 0, interval('5m'), 0),
+ 'server' => setv(T_FQDNP, 1, 0, 'api.godaddy.com/v1/domains', undef),
+ 'ttl' => setv(T_NUMBER, 1, 0, 600, undef),
+ 'zone' => setv(T_FQDN, 1, 0, '', undef),
+ },
+ },
+ 'googledomains' => {
+ 'updateable' => undef,
+ 'update' => \&nic_googledomains_update,
+ 'examples' => \&nic_googledomains_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'min-interval' => setv(T_DELAY, 0, 0, interval('5m'), 0),
+ 'server' => setv(T_FQDNP, 1, 0, 'domains.google.com', undef),
+ },
+ },
+ 'hetzner' => {
+ 'updateable' => undef,
+ 'update' => \&nic_hetzner_update,
+ 'examples' => \&nic_hetzner_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'login' => setv(T_LOGIN, 0, 0, 'token', undef),
+ 'min-interval' => setv(T_DELAY, 0, 0, interval('1m'), 0),
+ 'server' => setv(T_FQDNP, 1, 0, 'dns.hetzner.com/api/v1', undef),
+ 'ttl' => setv(T_NUMBER, 0, 0, 60, 60),
+ 'zone' => setv(T_FQDN, 1, 0, '', undef),
+ },
+ },
+ 'namecheap' => {
+ 'updateable' => undef,
+ 'update' => \&nic_namecheap_update,
+ 'examples' => \&nic_namecheap_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'min-interval' => setv(T_DELAY, 0, 0, 0, interval('5m')),
+ 'server' => setv(T_FQDNP, 1, 0, 'dynamicdns.park-your-domain.com', undef),
+ },
+ },
+ 'nfsn' => {
+ 'updateable' => undef,
+ 'update' => \&nic_nfsn_update,
+ 'examples' => \&nic_nfsn_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'min_interval' => setv(T_FQDNP, 0, 0, 0, interval('5m')),
+ 'server' => setv(T_FQDNP, 1, 0, 'api.nearlyfreespeech.net', undef),
+ 'ttl' => setv(T_NUMBER, 1, 0, 300, undef),
+ 'zone' => setv(T_FQDN, 1, 0, undef, undef),
+ },
+ },
+ 'njalla' => {
+ 'updateable' => undef,
+ 'update' => \&nic_njalla_update,
+ 'examples' => \&nic_njalla_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'login' => setv(T_STRING, 0, 0, 'unused', undef),
+ 'server' => setv(T_FQDNP, 1, 0, 'njal.la', undef),
+ 'quietreply' => setv(T_BOOL, 0, 1, 0, undef)
+ },
+ },
+ 'noip' => {
+ 'updateable' => undef,
+ 'update' => \&nic_noip_update,
+ 'examples' => \&nic_noip_examples,
+ 'variables' => {
+ 'atime' => setv(T_NUMBER, 0, 1, 0, undef),
+ 'custom' => setv(T_BOOL, 0, 1, 0, undef),
+ 'host' => setv(T_STRING, 1, 1, '', undef),
+ 'ip' => setv(T_IP, 0, 1, undef, undef),
+ 'login' => setv(T_LOGIN, 1, 0, '', undef),
+ 'max-interval' => setv(T_DELAY, 0, 0, interval('25d'), 0),
+ 'min-error-interval' => setv(T_DELAY, 0, 0, interval('5m'), 0),
+ 'min-interval' => setv(T_DELAY, 0, 0, interval('30s'), 0),
+ 'mtime' => setv(T_NUMBER, 0, 1, 0, undef),
+ 'password' => setv(T_PASSWD, 1, 0, '', undef),
+ 'server' => setv(T_FQDNP, 1, 0, 'dynupdate.no-ip.com', undef),
+ 'static' => setv(T_BOOL, 0, 1, 0, undef),
+ 'status' => setv(T_ANY, 0, 1, '', undef),
+ 'warned-min-error-interval' => setv(T_ANY, 0, 1, 0, undef),
+ 'warned-min-interval' => setv(T_ANY, 0, 1, 0, undef),
+ 'wtime' => setv(T_DELAY, 0, 1, 0, interval('30s')),
+ },
+ },
+ 'nsupdate' => {
+ 'updateable' => undef,
+ 'update' => \&nic_nsupdate_update,
+ 'examples' => \&nic_nsupdate_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'login' => setv(T_LOGIN, 1, 0, '/usr/bin/nsupdate', undef),
+ 'tcp' => setv(T_BOOL, 0, 1, 0, undef),
+ 'ttl' => setv(T_NUMBER, 0, 1, 600, undef),
+ 'zone' => setv(T_STRING, 1, 1, '', undef),
+ },
+ },
+ 'ovh' => {
+ 'updateable' => undef,
+ 'update' => \&nic_ovh_update,
+ 'examples' => \&nic_ovh_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'login' => setv(T_LOGIN, 1, 0, '', undef),
+ 'password' => setv(T_PASSWD, 1, 0, '', undef),
+ 'script' => setv(T_STRING, 1, 1, '/nic/update', undef),
+ 'server' => setv(T_FQDNP, 1, 0, 'www.ovh.com', undef),
+ },
+ },
+ 'sitelutions' => {
+ 'updateable' => undef,
+ 'update' => \&nic_sitelutions_update,
+ 'examples' => \&nic_sitelutions_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'server' => setv(T_FQDNP, 1, 0, 'www.sitelutions.com', undef),
+ 'min-interval' => setv(T_DELAY, 0, 0, 0, interval('5m')),
+ },
+ },
+ 'woima' => {
+ 'updateable' => undef,
+ 'update' => \&nic_woima_update,
+ 'examples' => \&nic_woima_examples,
+ 'variables' => {
+ 'atime' => setv(T_NUMBER, 0, 1, 0, undef),
+ 'backupmx' => setv(T_BOOL, 0, 1, 0, undef),
+ 'custom' => setv(T_BOOL, 0, 1, 0, undef),
+ 'ip' => setv(T_IP, 0, 1, undef, undef),
+ 'login' => setv(T_LOGIN, 1, 0, '', undef),
+ 'max-interval' => setv(T_DELAY, 0, 0, interval('25d'), 0),
+ 'min-error-interval' => setv(T_DELAY, 0, 0, interval('5m'), 0),
+ 'min-interval' => setv(T_DELAY, 0, 0, interval('30s'), 0),
+ 'mtime' => setv(T_NUMBER, 0, 1, 0, undef),
+ 'mx' => setv(T_OFQDN, 0, 1, '', undef),
+ 'password' => setv(T_PASSWD, 1, 0, '', undef),
+ 'script' => setv(T_STRING, 1, 1, '/nic/update', undef),
+ 'server' => setv(T_FQDNP, 1, 0, 'dyn.woima.fi', undef),
+ 'static' => setv(T_BOOL, 0, 1, 0, undef),
+ 'status' => setv(T_ANY, 0, 1, '', undef),
+ 'warned-min-error-interval' => setv(T_ANY, 0, 1, 0, undef),
+ 'warned-min-interval' => setv(T_ANY, 0, 1, 0, undef),
+ 'wildcard' => setv(T_BOOL, 0, 1, 0, undef),
+ 'wtime' => setv(T_DELAY, 0, 1, 0, interval('30s')),
+ },
+ },
+ 'yandex' => {
+ 'updateable' => undef,
+ 'update' => \&nic_yandex_update,
+ 'examples' => \&nic_yandex_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'min-interval' => setv(T_DELAY, 0, 0, interval('5m'), 0),
+ 'server' => setv(T_FQDNP, 1, 0, 'pddimp.yandex.ru', undef),
+ },
+ },
+ 'zoneedit1' => {
+ 'updateable' => undef,
+ 'update' => \&nic_zoneedit1_update,
+ 'examples' => \&nic_zoneedit1_examples,
+ 'variables' => {
+ %{$variables{'service-common-defaults'}},
+ 'min-interval' => setv(T_DELAY, 0, 0, interval('5m'), 0),
+ 'server' => setv(T_FQDNP, 1, 0, 'dynamic.zoneedit.com', undef),
+ 'zone' => setv(T_OFQDN, 0, 0, undef, undef),
+ },
+ },
+ 'keysystems' => {
+ 'updateable' => undef,
+ 'update' => \&nic_keysystems_update,
+ 'examples' => \&nic_keysystems_examples,
+ 'variables' => merge(
+ $variables{'keysystems-common-defaults'},
+ $variables{'service-common-defaults'},
+ ),
+ },
+ 'dnsexit' => {
+ 'updateable' => undef,
+ 'update' => \&nic_dnsexit_update,
+ 'examples' => \&nic_dnsexit_examples,
+ 'variables' => merge(
+ $variables{'dnsexit-common-defaults'},
+ $variables{'service-common-defaults'},
+ ),
+ },
+);
+$variables{'merged'} = {
+ map({ %{$services{$_}{'variables'}} } keys(%services)),
+ %{$variables{'dyndns-common-defaults'}},
+ %{$variables{'service-common-defaults'}},
+ %{$variables{'global-defaults'}},
+};
+
+# This will hold the processed args.
+my %opt = ();
+my $deprecated_handler = sub { warning("'-$_[0]' is deprecated and does nothing"); };
+$opt{'fw-banlocal'} = $deprecated_handler;
+$opt{'if-skip'} = $deprecated_handler;
+$opt{'list-devices'} = sub {
+ printf("%s %s\n", $_, $builtinfw{$_}{name}) for sort(keys(%builtinfw));
+ exit(0);
+};
+$opt{'list-protocols'} = sub {
+ printf("%s\n", $_) for sort(keys(%services));
+ exit(0);
+};
+$opt{'list-web-services'} = sub {
+ printf("%s %s\n", $_, $builtinweb{$_}{url}) for sort(keys(%builtinweb));
+ exit(0);
+};
+
+my @opt = (
+ "usage: ${program} [options]",
+ "options are:",
+ ["daemon", "=s", "-daemon : run as a daemon, specify as an interval"],
+ ["foreground", "!", "-foreground : do not fork"],
+ ["proxy", "=s", "-proxy : use as the HTTP proxy"],
+ ["server", "=s", "-server : update DNS information on "],
+ ["protocol", "=s", "-protocol : update protocol used"],
+ ["list-protocols", "", "-list-protocols : print a machine-readable list of supported update protocols and exit. Format: one per line"],
+ ["file", "=s", "-file : load configuration information from "],
+ ["cache", "=s", "-cache : record address used in "],
+ ["pid", "=s", "-pid : record process id in if daemonized"],
+ "",
+ ["use", "=s", "-use : deprecated, see 'usev4' and 'usev6'"],
+ &ip_strategies_usage(),
+ [ "usev4", "=s", "-usev4 : how the should IPv4 address be obtained."],
+ &ipv4_strategies_usage(),
+ [ "usev6", "=s", "-usev6 : how the should IPv6 address be obtained."],
+ &ipv6_strategies_usage(),
+ "",
+ " Options that apply to 'use=ip':",
+ ["ip", "=s", "-ip : deprecated, use 'ipv4' or 'ipv6'"],
+ ["ipv4", "=s", "-ipv4 : set the IPv4 address to "],
+ ["ipv6", "=s", "-ipv6 : set the IPv6 address to "],
+ "",
+ " Options that apply to 'use=if':",
+ ["if", "=s", "-if : deprecated, use 'ifv4' or 'ifv6'"],
+ ["ifv4", "=s", "-ifv4 : obtain IPv4 address from "],
+ ["ifv6", "=s", "-ifv6 : obtain IPv6 address from "],
+ "",
+ " Options that apply to 'use=web':",
+ ["web", "=s", "-web | : deprecated, use 'webv4' or 'webv6'"],
+ ["web-skip", "=s", "-web-skip : deprecated, use 'webv4-skip' or 'webv6-skip'"],
+ ["webv4", "=s", "-webv4 |: obtain IPv4 address from a web-based IP discovery service, either a known or a custom "],
+ ["webv4-skip", "=s", "-webv4-skip : skip any IP addresses before in the output of 'ip address show dev ' (or 'ifconfig ')"],
+ ["webv6", "=s", "-webv6 |: obtain IPv6 address from a web-based IP discovery service, either a known or a custom "],
+ ["webv6-skip", "=s", "-webv6-skip : skip any IP addresses before in the output of 'ip address show dev ' (or 'ifconfig ')"],
+ ["list-web-services", "", "-list-web-services : print a machine-readable list of web-based IP discovery services for use with 'web=' and exit. Format: one service per line, each line has the form ' '"],
+ "",
+ " Options that apply to 'use=fw' and 'use=':",
+ ["fw", "=s", "-fw | : deprecated, use 'fwv4' or 'fwv6'"],
+ ["fw-skip", "=s", "-fw-skip : deprecated, use 'fwv4-skip' or 'fwv6-skip'"],
+ ["fwv4", "=s", "-fwv4 | : obtain IPv4 address from device with IP address or URL "],
+ ["fwv4-skip", "=s", "-fwv4-skip : skip any IP addresses before in the text returned from the device"],
+ ["fwv6", "=s", "-fwv6 | : obtain IPv6 address from device with IP address or URL "],
+ ["fwv6-skip", "=s", "-fwv6-skip : skip any IP addresses before in the text returned from the device"],
+ ["fw-login", "=s", "-fw-login : use when getting the IP from the device"],
+ ["fw-password", "=s", "-fw-password : use password when getting the IP from the device"],
+ ["list-devices", "", "-list-devices : print a machine-readable list of supported firewall/router devices and exit. Format: one device per line, each line has the form ' '"],
+ "",
+ " Options that apply to 'use=cmd':",
+ ["cmd", "=s", "-cmd : deprecated, use 'cmdv4' or 'cmdv6'"],
+ ["cmd-skip", "=s", "-cmd-skip : deprecated, filter in program wrapper script"],
+ ["cmdv4", "=s", "-cmdv4 : obtain IPv4 address from the output of "],
+ ["cmdv6", "=s", "-cmdv6 : obtain IPv6 address from the output of "],
+ "",
+ ["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 "],
+ "",
+ ["options", "=s", "-options =[,=,...]\n : optional per-service arguments (see below)"],
+ "",
+ ["ssl", "!", "-{no}ssl : do updates over encrypted SSL connection"],
+ ["ssl_ca_dir", "=s", "-ssl_ca_dir : look in for certificates of trusted certificate authorities (default: auto-detect)"],
+ ["ssl_ca_file", "=s", "-ssl_ca_file : look at for certificates of trusted certificate authorities (default: auto-detect)"],
+ ["fw-ssl-validate", "!", "-{no}fw-ssl-validate : Validate SSL certificate when retrieving IP address from firewall"],
+ ["web-ssl-validate", "!","-{no}web-ssl-validate : Validate SSL certificate when retrieving IP address from web"],
+ ["curl", "!", "-{no}curl : use curl for network connections"],
+ ["retry", "!", "-{no}retry : retry failed updates"],
+ ["force", "!", "-{no}force : force an update even if the update may be unnecessary"],
+ ["timeout", "=i", "-timeout : when fetching a URL, wait at most seconds for a response"],
+ ["syslog", "!", "-{no}syslog : log messages to syslog"],
+ ["facility", "=s", "-facility : log messages to syslog to facility "],
+ ["priority", "=s", "-priority : log messages to syslog with priority "],
+ ["max-warn", "=i", "-max-warn : log at most warning messages for undefined IP address"],
+ ["mail", "=s", "-mail : e-mail messages to "],
+ ["mail-failure", "=s", "-mail-failure : e-mail messages for failed updates to "],
+ ["exec", "!", "-{no}exec : do {not} execute; just show what would be done"],
+ ["debug", "!", "-{no}debug : print {no} debugging information"],
+ ["verbose", "!", "-{no}verbose : print {no} verbose information"],
+ ["quiet", "!", "-{no}quiet : print {no} messages for unnecessary updates"],
+ ["help", "", "-help : display this message and exit"],
+ ["postscript", "", "-postscript : script to run after updating ddclient, has new IP as param"],
+ ["query", "!", "-{no}query : print {no} ip addresses and exit"],
+ ["fw-banlocal", "!", ""], ## deprecated
+ ["if-skip", "=s", ""], ## deprecated
+ ["test", "!", ""], ## hidden
+ ["geturl", "=s", ""], ## hidden
+ "",
+ nic_examples(),
+ "$program version $version, ",
+ " originally written by Paul Burry, paul+ddclient\@burry.ca",
+ " project now maintained on https://github.com/ddclient/ddclient"
+);
+
+sub main {
+ ## process args
+ my $opt_usage = process_args(@opt);
+ $saved_cache = '';
+ %saved_opt = %opt;
+ $result = 'OK';
+
+ test_geturl(opt('geturl')) if opt('geturl');
+
+ if (opt('help')) {
+ printf "%s\n", $opt_usage;
+ exit 0;
+ }
+
+ ## read config file because 'daemon' mode may be defined there.
+ read_config($opt{'file'} // default('file'), \%config, \%globals);
+ init_config();
+ test_possible_ip() if opt('query');
+
+ my $caught_hup = 0;
+ my $caught_term = 0;
+ my $caught_int = 0;
+ $SIG{'HUP'} = sub { $caught_hup = 1; };
+ $SIG{'TERM'} = sub { $caught_term = 1; };
+ $SIG{'INT'} = sub { $caught_int = 1; };
+ # don't fork() if foreground
+ if (opt('foreground')) {
+ ;
+ } elsif (opt('daemon')) {
+ $SIG{'CHLD'} = 'IGNORE';
+ my $pid = fork;
+ if ($pid < 0) {
+ print STDERR "${program}: can not fork ($!)\n";
+ exit -1;
+ } elsif ($pid) {
+ exit 0;
+ }
+ $SIG{'CHLD'} = 'DEFAULT';
+ open(STDOUT, ">/dev/null");
+ open(STDERR, ">/dev/null");
+ open(STDIN, " 0) && !$caught_hup && !$caught_term && !$caught_int) {
+ my $delay = $left > 10 ? 10 : $left;
+
+ $0 = sprintf("%s - sleeping for %s seconds", $program, $left);
+ $left -= sleep $delay;
+ # preventing deep sleep - see [bugs:#46]
+ if ($left > $daemon) {
+ $left = $daemon;
+ }
+ }
+ $caught_hup = 0;
+ $result = 0;
+
+ } elsif (!scalar(%config)) {
+ warning("no hosts to update.") unless !opt('quiet') || opt('verbose') || !$daemon;
+ $result = 1;
+
+ } else {
+ $result = $result eq 'OK' ? 0 : 1;
+ }
+ } while ($daemon && !$result && !$caught_term && !$caught_int);
+
+ warning("caught SIGINT; exiting") if $caught_int;
+ unlink_pid();
+ sendmail();
+
+ exit($result);
+}
+
+######################################################################
+## runpostscript
+######################################################################
+
+sub runpostscript {
+ my ($ip) = @_;
+
+ if (defined $globals{postscript}) {
+ if (-x $globals{postscript}) {
+ system("$globals{postscript} $ip &");
+ } else {
+ warning("Can not execute post script: %s", $globals{postscript});
+ }
+ }
+}
+
+######################################################################
+## update_nics
+######################################################################
+sub update_nics {
+ my %examined = ();
+ my %iplist = ();
+ my %ipv4list = ();
+ my %ipv6list = ();
+
+ foreach my $s (sort keys %services) {
+ my (@hosts, %ipsv4, %ipsv6) = ();
+ my $updateable = $services{$s}{'updateable'};
+ my $update = $services{$s}{'update'};
+
+ foreach my $h (sort keys %config) {
+ next if $config{$h}{'protocol'} ne lc($s);
+ $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
+ my $arg_ip = opt('ip', $h) // '';
+ my $arg_ipv4 = opt('ipv4', $h) // '';
+ my $arg_ipv6 = opt('ipv6', $h) // '';
+ my $arg_fw = opt('fw', $h) // '';
+ my $arg_fwv4 = opt('fwv4', $h) // '';
+ my $arg_fwv6 = opt('fwv6', $h) // '';
+ my $arg_if = opt('if', $h) // '';
+ my $arg_ifv4 = opt('ifv4', $h) // '';
+ my $arg_ifv6 = opt('ifv6', $h) // '';
+ my $arg_web = opt('web', $h) // '';
+ my $arg_webv4 = opt('webv4', $h) // '';
+ my $arg_webv6 = opt('webv6', $h) // '';
+ my $arg_cmd = opt('cmd', $h) // '';
+ my $arg_cmdv4 = opt('cmdv4', $h) // '';
+ my $arg_cmdv6 = opt('cmdv6', $h) // '';
+ my $ip = undef;
+ my $ipv4 = undef;
+ my $ipv6 = undef;
+
+ if ($use ne 'disabled') {
+ if (exists $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd}) {
+ # If we have already done a get_ip() for this, don't do it again.
+ $ip = $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd};
+ } else {
+ # Else need to find the IP address...
+ $ip = get_ip($use, $h);
+ if (is_ipv4($ip) || is_ipv6($ip)) {
+ # And if it is valid, remember it...
+ $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd} = $ip;
+ } else {
+ warning("%s: unable to determine IP address with strategy use=%s", $h, $use)
+ if !$daemon || opt('verbose');
+ }
+ }
+ # And remember it as the IP address we want to send to the DNS service.
+ $config{$h}{'wantip'} = $ip;
+ }
+
+ if ($usev4 ne 'disabled') {
+ if (exists $ipv4list{$usev4}{$arg_ipv4}{$arg_fwv4}{$arg_ifv4}{$arg_webv4}{$arg_cmdv4}) {
+ # If we have already done a get_ipv4() for this, don't do it again.
+ $ipv4 = $ipv4list{$usev4}{$arg_ipv4}{$arg_fwv4}{$arg_ifv4}{$arg_webv4}{$arg_cmdv4};
+ } else {
+ # Else need to find the IPv4 address...
+ $ipv4 = get_ipv4($usev4, $h);
+ if (is_ipv4($ipv4)) {
+ # And if it is valid, remember it...
+ $ipv4list{$usev4}{$arg_ipv4}{$arg_fwv4}{$arg_ifv4}{$arg_webv4}{$arg_cmdv4} = $ipv4;
+ } else {
+ warning("%s: unable to determine IPv4 address with strategy usev4=%s", $h, $usev4)
+ if !$daemon || opt('verbose');
+ }
+ }
+ # And remember it as the IPv4 address we want to send to the DNS service.
+ $config{$h}{'wantipv4'} = $ipv4;
+ }
+
+ if ($usev6 ne 'disabled') {
+ if (exists $ipv6list{$usev6}{$arg_ipv6}{$arg_fwv6}{$arg_ifv6}{$arg_webv6}{$arg_cmdv6}) {
+ # If we have already done a get_ipv6() for this, don't do it again.
+ $ipv6 = $ipv6list{$usev6}{$arg_ipv6}{$arg_fwv6}{$arg_ifv6}{$arg_webv6}{$arg_cmdv6};
+ } else {
+ # Else need to find the IPv6 address...
+ $ipv6 = get_ipv6($usev6, $h);
+ if (is_ipv6($ipv6)) {
+ # And if it is valid, remember it...
+ $ipv6list{$usev6}{$arg_ipv6}{$arg_fwv6}{$arg_ifv6}{$arg_webv6}{$arg_cmdv6} = $ipv6;
+ } else {
+ warning("%s: unable to determine IPv6 address with strategy usev6=%s", $h, $usev6)
+ if !$daemon || opt('verbose');
+ }
+ }
+ # And remember it as the IP address we want to send to the DNS service.
+ $config{$h}{'wantipv6'} = $ipv6;
+ }
+
+ # DNS service update functions should only have to handle 'wantipv4' and 'wantipv6'
+ $config{$h}{'wantipv4'} = $ipv4 = $ip if (!$ipv4 && is_ipv4($ip));
+ $config{$h}{'wantipv6'} = $ipv6 = $ip if (!$ipv6 && is_ipv6($ip));
+ # But we will set 'wantip' to the IPv4 so old functions continue to work until we update them all
+ $config{$h}{'wantip'} = $ipv4 if (!$ip && $ipv4);
+
+ next if !nic_updateable($h, $updateable);
+ push @hosts, $h;
+
+ $ipsv4{$ipv4} = $h if ($ipv4);
+ $ipsv6{$ipv6} = $h if ($ipv6);
+ }
+ if (@hosts) {
+ $0 = sprintf("%s - updating %s", $program, join(',', @hosts));
+ &$update(@hosts);
+ runpostscript(join ' ', keys %ipsv4, keys %ipsv6);
+ }
+ }
+ foreach my $h (sort keys %config) {
+ if (!exists $examined{$h}) {
+ failed("%s was not updated because protocol %s is not supported.",
+ $h, $config{$h}{'protocol'} // '');
+ }
+ }
+ write_cache(opt('cache'));
+}
+
+######################################################################
+## unlink_pid()
+######################################################################
+sub unlink_pid {
+ if (opt('pid') && opt('daemon')) {
+ unlink opt('pid');
+ }
+}
+
+######################################################################
+## write_pid()
+######################################################################
+sub write_pid {
+ my $file = opt('pid');
+
+ if ($file && opt('daemon')) {
+ local *FD;
+ if (!open(FD, "> $file")) {
+ warning("Cannot create file '%s'. (%s)", $file, $!);
+
+ } else {
+ printf FD "%s\n", $$;
+ close(FD);
+ }
+ }
+}
+
+######################################################################
+## write_cache($file)
+######################################################################
+sub write_cache {
+ my ($file) = @_;
+
+ ## merge the updated host entries into the cache.
+ foreach my $h (keys %config) {
+ if (!exists $cache{$h} || $config{$h}{'update'}) {
+ map { defined($config{$h}{$_}) ? ($cache{$h}{$_} = $config{$h}{$_}) : () } @{$config{$h}{'cacheable'}};
+ } else {
+ map { $cache{$h}{$_} = $config{$h}{$_} } qw(atime wtime status);
+ }
+ }
+
+ ## construct the cache file.
+ my $cache = "";
+ foreach my $h (sort keys %cache) {
+ my $opt = join(',', map { "$_=" . ($cache{$h}{$_} // '') } sort keys %{$cache{$h}});
+
+ $cache .= sprintf "%s%s%s\n", $opt, ($opt ? ' ' : ''), $h;
+ }
+ $file = '' if defined($saved_cache) && $cache eq $saved_cache;
+
+ ## write the updates and other entries to the cache file.
+ if ($file) {
+ (undef, my $dir) = fileparse($file);
+ make_path($dir, { error => \my $err }) if !-d $dir;
+ if ($err && @$err) {
+ for my $diag (@$err) {
+ my ($f, $msg) = %$diag;
+ warning("Failed to create cache file directory: %s: %s", $f, $msg);
+ }
+ return;
+ }
+
+ $saved_cache = undef;
+ local *FD;
+ if (!open(FD, ">", $file)) {
+ warning("Failed to create cache file %s: %s", $file, $!);
+ return;
+ }
+ printf FD "## %s-%s\n", $program, $version;
+ printf FD "## last updated at %s (%d)\n", prettytime($now), $now;
+ printf FD "%s", $cache;
+
+ close(FD);
+ }
+}
+######################################################################
+## read_cache($file) - called before reading the .conf
+######################################################################
+sub read_cache {
+ my $file = shift;
+ my $config = shift;
+ my $globals = {};
+
+ %{$config} = ();
+ ## read the cache file ignoring anything on the command-line.
+ if (-e $file) {
+ my %saved = %opt;
+ %opt = ();
+ $saved_cache = _read_config($config, $globals, "##\\s*$program-$version\\s*", $file);
+ %opt = %saved;
+
+ foreach my $h (keys %cache) {
+ if (exists $config->{$h}) {
+ foreach (qw(atime mtime wtime ip status)) {
+ $config->{$h}{$_} = $cache{$h}{$_} if exists $cache{$h}{$_};
+ }
+ }
+ }
+ }
+}
+######################################################################
+## parse_assignments(string) return (rest, %variables)
+## parse_assignment(string) return (name, value, rest)
+######################################################################
+sub parse_assignments {
+ my ($rest) = @_;
+ my %variables = ();
+
+ while (1) {
+ (my $name, my $value, $rest) = parse_assignment($rest);
+ $rest =~ s/^[,\s]+//;
+ return ($rest, %variables) if !defined($name);
+ if ($name eq 'fw-banlocal' || $name eq 'if-skip') {
+ warning("'$name' is deprecated and does nothing");
+ next;
+ }
+ $variables{$name} = $value;
+ }
+}
+sub parse_assignment {
+ my ($rest) = @_;
+ my ($name, $value);
+ my ($escape, $quote) = (0, '');
+
+ if ($rest =~ /^[,\s]*([a-z][0-9a-z_-]*)=(.*)/i) {
+ ($name, $rest, $value) = ($1, $2, '');
+
+ while (length(my $c = substr($rest, 0, 1))) {
+ if ($escape) {
+ $value .= $c;
+ $escape = 0;
+ } elsif ($c eq "\\") {
+ $escape = 1;
+ } elsif ($quote && $c eq $quote) {
+ $quote = '';
+ } elsif (!$quote && $c =~ /[\'\"]/) {
+ $quote = $c;
+ } elsif (!$quote && $c =~ /^[\n\s,]/) {
+ # The terminating character is not consumed.
+ last;
+ } else {
+ $value .= $c;
+ }
+ $rest = substr($rest,1);
+ }
+ }
+ 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;
+ return ($name, $value, $rest);
+}
+######################################################################
+## read_config
+######################################################################
+sub read_config {
+ my ($file, $config, $globals) = @_;
+ _read_config($config, $globals, '', $file);
+}
+sub _read_config {
+ # Configuration line format after comment and continuation
+ # removal:
+ #
+ # [opt=value, ...] [host[, ...] [login [password]]]
+ #
+ # Details:
+ # - No whitespace is allowed around the '=' in opt=value.
+ # - An option name may only contain lowercase letters, numbers,
+ # underscore, and hyphen-minus, and must start with a letter.
+ # - A value or hostname is terminated by unquoted whitespace
+ # (including newline) or an unquoted comma followed by
+ # optional whitespace.
+ # - Values (but not hosts, login, or password) may contain
+ # quoted parts:
+ # - A backslash that itself is not quoted by another
+ # backslash quotes the next character.
+ # - An unquoted single quote quotes the subsequent
+ # non-backslash, non-newline characters until the next
+ # single quote.
+ # - An unquoted double quote quotes the subsequent
+ # non-backslash, non-newline characters until the next
+ # double quote.
+ # - login and password must not contain whitespace.
+ # - login must not start or end with a comma.
+ # - password must not start with a comma.
+ # - If no host is specified (either via a 'host=' option or
+ # after the options), the options are stored in %{$2}.
+ # Otherwise, the options are combined with the global values
+ # accumulated thus far and stored in $1->{$host} for each
+ # referenced host.
+
+ my $config = shift;
+ my $globals = shift;
+ my $stamp = shift;
+ local $file = shift;
+ my %globals = ();
+ my %config = ();
+ my $content = '';
+
+ local *FD;
+ if (!open(FD, "< $file")) {
+ warning("Cannot open file '%s'. (%s)", $file, $!);
+ }
+
+ # If file is owned by our effective uid, ensure that it has no access for group or others.
+ # Otherwise, require that it isn't writable when not owned by us. For example allow it to
+ # be owned by root:ddclient with mode 640. Always ensure that it is not accessible to others.
+ my ($dev, $ino, $mode, @statrest) = stat(FD);
+ if ($mode & 077 && -o FD) {
+ if (-f FD && (chmod 0600, $file)) {
+ warning("file $file must be accessible only by its owner (fixed).");
+ }
+ warning("file $file must be accessible only by its owner.");
+ } elsif (! -o FD && -w FD) {
+ warning("file $file should be owned only by ddclient or not be writable.");
+ }
+ if ($mode & 07) {
+ warning("file $file must not be accessible by others.");
+ }
+
+ local $lineno = 0;
+ my $continuation = '';
+ my %passwords = ();
+ while () {
+ s/[\r\n]//g;
+
+ $lineno++;
+
+ ## check for the program version stamp
+ if (($. == 1) && $stamp && ($_ !~ /^$stamp$/i)) {
+ warning("program version mismatch; ignoring %s", $file);
+ last;
+ }
+ if (/\\\s+$/) {
+ warning("whitespace follows the \\ at the end-of-line.\nIf you meant to have a line continuation, remove the trailing whitespace.");
+ }
+
+ $content .= "$_\n" unless /^#/;
+
+ ## parsing passwords is special
+ if (/^([^#]*\s)?([^#]*?password\S*?)\s*=\s*('.*'|[^']\S*)(.*)/) {
+ my ($head, $key, $value, $tail) = ($1 // '', $2, $3, $4);
+ $value = $1 if $value =~ /^'(.*)'$/;
+ $passwords{$key} = $value;
+ $_ = "${head}${key}=dummy${tail}";
+ }
+
+ ## remove comments
+ s/#.*//;
+
+ ## handle continuation lines
+ $_ = "$continuation$_";
+ if (/\\$/) {
+ chop;
+ $continuation = $_;
+ next;
+ }
+ $continuation = '';
+
+ s/^\s+//; # remove leading white space
+ s/\s+$//; # remove trailing white space
+ s/\s+/ /g; # canonify
+ next if /^$/;
+
+ my %locals;
+ ($_, %locals) = parse_assignments($_);
+ s/\s*,\s*/,/g;
+ my @args = split;
+
+ ## verify that keywords are valid...and check the value
+ foreach my $k (keys %locals) {
+ $locals{$k} = $passwords{$k} if defined $passwords{$k};
+ if (!exists $variables{'merged'}{$k}) {
+ warning("unrecognized keyword '%s' (ignored)", $k);
+ delete $locals{$k};
+ } else {
+ 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});
+ delete $locals{$k};
+ } else { $locals{$k} = $value; }
+ }
+ }
+ if (exists($locals{'host'})) {
+ $args[0] = @args ? "$args[0],$locals{host}" : "$locals{host}";
+ }
+ ## accumulate globals
+ if ($#args < 0) {
+ map { $globals{$_} = $locals{$_} } keys %locals;
+ }
+
+ ## process this host definition
+ if (@args) {
+ my ($host, $login, $password) = @args;
+
+ ## add in any globals..
+ %locals = %{merge(\%locals, \%globals)};
+
+ ## override login and password if specified the old way.
+ $locals{'login'} = $login if defined $login;
+ $locals{'password'} = $password if defined $password;
+
+ ## allow {host} to be a comma separated list of hosts
+ foreach my $h (split_by_comma($host)) {
+ ## save a copy of the current globals
+ $config{$h} = { %locals };
+ $config{$h}{'host'} = $h;
+ }
+ }
+ %passwords = ();
+ }
+ close(FD);
+
+ warning("file ends while expecting a continuation line.")
+ if $continuation;
+
+ %$globals = %globals;
+ %$config = %config;
+
+ return $content;
+}
+######################################################################
+## init_config -
+######################################################################
+sub init_config {
+ %opt = %saved_opt;
+
+ ##
+ $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') > 0 && opt('daemon') < minimum('daemon');
+
+ ## define or modify host options specified on the command-line
+ if (exists $opt{'options'} && defined $opt{'options'}) {
+ ## collect cmdline configuration options.
+ my %options = ();
+ foreach 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;
+ }
+ ## determine hosts specified with -host
+ my @hosts = ();
+ if (exists $opt{'host'}) {
+ foreach my $h (split_by_comma($opt{'host'})) {
+ push @hosts, $h;
+ }
+ }
+ ## and those in -options=...
+ if (exists $options{'host'}) {
+ foreach my $h (split_by_comma($options{'host'})) {
+ push @hosts, $h;
+ }
+ delete $options{'host'};
+ }
+ ## merge options into host definitions or globals
+ if (@hosts) {
+ foreach my $h (@hosts) {
+ $config{$h} = merge(\%options, $config{$h});
+ }
+ $opt{'host'} = join(',', @hosts);
+ } else {
+ %globals = %{merge(\%options, \%globals)};
+ }
+ }
+
+ ## override global options with those on the command-line.
+ foreach my $o (keys %opt) {
+ if (defined $opt{$o} && exists $variables{'global-defaults'}{$o}) {
+ $globals{$o} = $opt{$o};
+ }
+ }
+
+ ## sanity check
+ if (defined $opt{'host'} && defined $opt{'retry'}) {
+ fatal("options -retry and -host (or -option host=..) are mutually exclusive");
+ }
+
+ ## determine hosts to update (those on the cmd-line, config-file, or failed cached)
+ my @hosts = keys %config;
+ if (opt('host')) {
+ @hosts = split_by_comma($opt{'host'});
+ }
+ if (opt('retry')) {
+ @hosts = map { $_ if $cache{$_}{'status'} ne 'good' } keys %cache;
+ }
+
+ ## remove any other hosts
+ my %hosts;
+ map { $hosts{$_} = undef } @hosts;
+ map { delete $config{$_} unless exists $hosts{$_} } keys %config;
+
+ ## collect the cacheable variables.
+ foreach my $proto (keys %services) {
+ my @cacheable = ();
+ foreach my $k (keys %{$services{$proto}{'variables'}}) {
+ push @cacheable, $k if $services{$proto}{'variables'}{$k}{'cache'};
+ }
+ $services{$proto}{'cacheable'} = [ @cacheable ];
+ }
+
+ ## sanity check..
+ ## make sure config entries have all defaults and they meet minimums
+ ## first the globals...
+ foreach my $k (keys %globals) {
+ my $def = $variables{'merged'}{$k};
+ my $ovalue = $globals{$k} // $def->{'default'};
+ my $value = check_value($ovalue, $def);
+ if ($def->{'required'} && !defined $value) {
+ $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:
+ foreach my $h (keys %config) {
+ my $proto;
+ $proto = $config{$h}{'protocol'};
+ $proto = opt('protocol') if !defined($proto);
+
+ load_sha1_support($proto) if (grep (/^$proto$/, ("freedns", "nfsn")));
+ load_json_support($proto) if (grep (/^$proto$/, ("1984", "cloudflare", "gandi", "godaddy", "hetzner", "yandex", "nfsn", "njalla")));
+
+ if (!exists($services{$proto})) {
+ warning("skipping host: %s: unrecognized protocol '%s'", $h, $proto);
+ delete $config{$h};
+
+ } else {
+ my $svars = $services{$proto}{'variables'};
+ my $conf = { 'protocol' => $proto };
+
+ foreach my $k (keys %$svars) {
+ my $def = $svars->{$k};
+ my $ovalue = $config{$h}{$k} // $def->{'default'};
+ my $value = check_value($ovalue, $def);
+ if ($def->{'required'} && !defined $value) {
+ warning("skipping host: %s: '%s=%s' is an invalid %s.", $h, $k, $ovalue, $def->{'type'});
+ delete $config{$h};
+ next HOST;
+ }
+ $conf->{$k} = $value;
+
+ }
+ $config{$h} = $conf;
+ $config{$h}{'cacheable'} = [ @{$services{$proto}{'cacheable'}} ];
+ }
+ }
+}
+
+######################################################################
+## process_args -
+######################################################################
+sub process_args {
+ my @spec = ();
+ my $usage = "";
+
+ foreach (@_) {
+ if (ref $_) {
+ my ($key, $specifier, $arg_usage) = @$_;
+ my $value = default($key);
+
+ ## add a option specifier
+ push @spec, $key . $specifier;
+
+ ## define the default value which can be overwritten later
+ $opt{$key} = undef unless exists($opt{$key});
+
+ next unless $arg_usage;
+
+ ## add a line to the usage;
+ $usage .= " $arg_usage";
+ if (defined($value) && $value ne '') {
+ $usage .= " (default: ";
+ if ($specifier eq '!') {
+ $usage .= "no" if ($specifier eq '!') && !$value;
+ $usage .= $key;
+ } else {
+ $usage .= $value;
+ }
+ $usage .= ")";
+ }
+ $usage .= ".";
+ } else {
+ $usage .= $_;
+ }
+ $usage .= "\n";
+ }
+ ## process the arguments
+ if (!GetOptions(\%opt, @spec)) {
+ $opt{"help"} = 1;
+ }
+ return $usage;
+}
+
+######################################################################
+## test_possible_ip - print possible IPs
+######################################################################
+sub test_possible_ip {
+ local $opt{'debug'} = 0;
+
+ printf "----- Test_possible_ip with 'get_ip' -----\n";
+ printf "use=ip, ip=%s address is %s\n", opt('ip'), get_ip('ip') // 'NOT FOUND'
+ if defined opt('ip');
+
+ {
+ local $opt{'use'} = 'if';
+ # Note: The `ip` command adds a `@eth0` suffix to the names of VLAN
+ # interfaces. That `@eth0` suffix is NOT part of the interface name.
+ my @ifs = map({ /^[^\s:]*:\s*([^\s:@]+)/ ? $1 : () }
+ `command -v ip >/dev/null && ip -o link show`);
+ @ifs = map({ /^([a-zA-Z].*?)(?::?\s.*)?$/ ? $1 : () }
+ `command -v ifconfig >/dev/null && ifconfig -a`) if $? || !@ifs;
+ @ifs = () if $?;
+ warning("failed to get list of interfaces") if !@ifs;
+ foreach my $if (@ifs) {
+ local $opt{'if'} = $if;
+ printf "use=if, if=%s address is %s\n", opt('if'), get_ip('if') // 'NOT FOUND';
+ }
+ }
+ if (opt('fw')) {
+ if (opt('fw') !~ m%/%) {
+ foreach my $fw (sort keys %builtinfw) {
+ local $opt{'use'} = $fw;
+ printf "use=%s address is %s\n", $fw, get_ip($fw) // 'NOT FOUND';
+ }
+ }
+ local $opt{'use'} = 'fw';
+ printf "use=fw, fw=%s address is %s\n", opt('fw'), get_ip(opt('fw')) // 'NOT FOUND'
+ if !exists $builtinfw{opt('fw')};
+
+ }
+ {
+ local $opt{'use'} = 'web';
+ foreach my $web (sort keys %builtinweb) {
+ local $opt{'web'} = $web;
+ printf "use=web, web=%s address is %s\n", $web, get_ip('web') // 'NOT FOUND';
+ }
+ printf "use=web, web=%s address is %s\n", opt('web'), get_ip('web') // 'NOT FOUND'
+ if !exists $builtinweb{opt('web')};
+ }
+ if (opt('cmd')) {
+ local $opt{'use'} = 'cmd';
+ printf "use=cmd, cmd=%s address is %s\n", opt('cmd'), get_ip('cmd') // 'NOT FOUND';
+ }
+
+ # Now force IPv4
+ printf "----- Test_possible_ip with 'get_ipv4' ------\n";
+ printf "use=ipv4, ipv4=%s address is %s\n", opt('ipv4'), get_ipv4('ipv4') // 'NOT FOUND'
+ if defined opt('ipv4');
+
+ {
+ # Note: The `ip` command adds a `@eth0` suffix to the names of VLAN
+ # interfaces. That `@eth0` suffix is NOT part of the interface name.
+ my @ifs = map({ /^[^\s:]*:\s*([^\s:@]+)/ ? $1 : () }
+ `command -v ip >/dev/null && ip -o link show`);
+ @ifs = map({ /^([a-zA-Z].*?)(?::?\s.*)?$/ ? $1 : () }
+ `command -v ifconfig >/dev/null && ifconfig -a`) if $? || !@ifs;
+ @ifs = () if $?;
+ warning("failed to get list of interfaces") if !@ifs;
+ foreach my $if (@ifs) {
+ local $opt{'ifv4'} = $if;
+ printf "use=ifv4, ifv4=%s address is %s\n", opt('ifv4'), get_ipv4('ifv4') // 'NOT FOUND';
+ }
+ }
+ {
+ local $opt{'usev4'} = 'webv4';
+ foreach my $web (sort keys %builtinweb) {
+ local $opt{'webv4'} = $web;
+ printf "use=webv4, webv4=$web address is %s\n", get_ipv4('webv4') // 'NOT FOUND'
+ if ($web !~ "6") ## Don't bother if web site only supports IPv6;
+ }
+ printf "use=webv4, webv4=%s address is %s\n", opt('webv4'), get_ipv4('webv4') // 'NOT FOUND'
+ if ! exists $builtinweb{opt('webv4')};
+ }
+ if (opt('cmdv4')) {
+ local $opt{'usev4'} = 'cmdv4';
+ printf "use=cmdv4, cmdv4=%s address is %s\n", opt('cmdv4'), get_ipv4('cmdv4') // 'NOT FOUND';
+ }
+
+ # Now force IPv6
+ printf "----- Test_possible_ip with 'get_ipv6' -----\n";
+ printf "use=ipv6, ipv6=%s address is %s\n", opt('ipv6'), get_ipv6('ipv6') // 'NOT FOUND'
+ if defined opt('ipv6');
+
+ {
+ # Note: The `ip` command adds a `@eth0` suffix to the names of VLAN
+ # interfaces. That `@eth0` suffix is NOT part of the interface name.
+ my @ifs = map({ /^[^\s:]*:\s*([^\s:@]+)/ ? $1 : () }
+ `command -v ip >/dev/null && ip -o link show`);
+ @ifs = map({ /^([a-zA-Z].*?)(?::?\s.*)?$/ ? $1 : () }
+ `command -v ifconfig >/dev/null && ifconfig -a`) if $? || !@ifs;
+ @ifs = () if $?;
+ warning("failed to get list of interfaces") if !@ifs;
+ foreach my $if (@ifs) {
+ local $opt{'ifv6'} = $if;
+ printf "use=ifv6, ifv6=%s address is %s\n", opt('ifv6'), get_ipv6('ifv6') // 'NOT FOUND';
+ }
+ }
+ {
+ local $opt{'usev6'} = 'webv6';
+ foreach my $web (sort keys %builtinweb) {
+ local $opt{'webv6'} = $web;
+ printf "use=webv6, webv6=$web address is %s\n", get_ipv6('webv6') // 'NOT FOUND'
+ if ($web !~ "4"); ## Don't bother if web site only supports IPv4
+ }
+ printf "use=webv6, webv6=%s address is %s\n", opt('webv6'), get_ipv6('webv6') // 'NOT FOUND'
+ if ! exists $builtinweb{opt('webv6')};
+ }
+ if (opt('cmdv6')) {
+ local $opt{'usev6'} = 'cmdv6';
+ printf "use=cmdv6, cmdv6=%s address is %s\n", opt('cmdv6'), get_ipv6('cmdv6') // 'NOT FOUND';
+ }
+
+ exit 0 unless opt('debug');
+}
+######################################################################
+## test_geturl - print (and save if -test) result of fetching a URL
+######################################################################
+sub test_geturl {
+ my $url = shift;
+
+ my $reply = geturl(
+ proxy => opt('proxy'),
+ url => $url,
+ login => opt('login'),
+ password => opt('password'),
+ );
+ print "URL $url\n";
+ print $reply // "\n";
+ exit;
+}
+######################################################################
+## load_file
+######################################################################
+sub load_file {
+ my $file = shift;
+ my $buffer = '';
+
+ if (exists($ENV{'TEST_CASE'})) {
+ my $try = "$file-$ENV{'TEST_CASE'}";
+ $file = $try if -f $try;
+ }
+
+ local *FD;
+ if (open(FD, "< $file")) {
+ read(FD, $buffer, -s FD);
+ close(FD);
+ debug("Loaded %d bytes from %s", length($buffer), $file);
+ } else {
+ debug("Load failed from %s (%s)", $file, $!);
+ }
+ return $buffer;
+}
+######################################################################
+## save_file
+######################################################################
+sub save_file {
+ my ($file, $buffer, $opt) = @_;
+
+ $file .= "-$ENV{'TEST_CASE'}" if exists $ENV{'TEST_CASE'};
+ if (defined $opt) {
+ my $i = 0;
+ while (-f "$file-$i") {
+ if ('unique' =~ /^$opt/i) {
+ my $a = join('\n', grep {!/^Date:/} split /\n/, $buffer);
+ my $b = join('\n', grep {!/^Date:/} split /\n/, load_file("$file-$i"));
+ last if $a eq $b;
+ }
+ $i++;
+ }
+ $file = "$file-$i";
+ }
+ debug("Saving to %s", $file);
+ local *FD;
+ open(FD, "> $file") or return;
+ print FD $buffer;
+ close(FD);
+ return $buffer;
+}
+######################################################################
+## print_opt
+## print_globals
+## print_config
+## print_cache
+## print_info
+######################################################################
+sub _print_hash {
+ my ($string, $ptr) = @_;
+ my $value = $ptr;
+
+ if (!defined($ptr)) {
+ $value = "";
+ } elsif (ref $ptr eq 'HASH') {
+ foreach my $key (sort keys %$ptr) {
+ if (($key eq "login") || ($key eq "password")) {
+ $value = "";
+ } else {
+ $value = $ptr->{$key};
+ }
+ _print_hash("${string}\{$key\}", $value);
+ }
+ return;
+ }
+ printf "%-36s : %s\n", $string, $value;
+}
+sub print_hash {
+ my ($string, $hash) = @_;
+ printf "=== %s ====\n", $string;
+ _print_hash($string, $hash);
+}
+sub print_opt { print_hash("opt", \%opt); }
+sub print_globals { print_hash("globals", \%globals); }
+sub print_config { print_hash("config", \%config); }
+sub print_cache { print_hash("cache", \%cache); }
+sub print_info {
+ print_opt();
+ print_globals();
+ print_config();
+ print_cache();
+}
+######################################################################
+## pipecmd - run an external command
+## logger
+## sendmail
+######################################################################
+sub pipecmd {
+ my $cmd = shift;
+ my $stdin = join("\n", @_);
+ my $ok = 0;
+
+ ## remove trailing newlines
+ 1 while chomp($stdin);
+
+ ## override when debugging.
+ $cmd = opt('exec') ? "| $cmd" : "> /dev/null";
+
+ ## execute the command.
+ local *FD;
+ if (!open(FD, $cmd)) {
+ printf STDERR "%s: cannot execute command %s.\n", $program, $cmd;
+
+ } elsif ($stdin && (!print FD "$stdin\n")) {
+ printf STDERR "%s: failed writting to %s.\n", $program, $cmd;
+ close(FD);
+
+ } elsif (!close(FD)) {
+ printf STDERR "%s: failed closing %s.(%s)\n", $program, $cmd, $@;
+
+ } elsif (opt('exec') && $?) {
+ printf STDERR "%s: failed %s. (%s)\n", $program, $cmd, $@;
+
+ } else {
+ $ok = 1;
+ }
+ return $ok;
+}
+sub logger {
+ if (opt('syslog') && opt('facility') && opt('priority')) {
+ my $facility = opt('facility');
+ my $priority = opt('priority');
+ return pipecmd("logger -p$facility.$priority -t${program}\[$$\]", @_);
+ }
+ return 1;
+}
+sub sendmail {
+ my $recipients = opt('mail');
+
+ if (opt('mail-failure') && ($result ne 'OK' && $result ne '0')) {
+ $recipients = opt('mail-failure');
+ }
+ if ($msgs && $recipients && $msgs ne $last_msgs) {
+ pipecmd("sendmail -oi $recipients",
+ "To: $recipients",
+ "Subject: status report from $program\@$hostname",
+ "\r\n",
+ $msgs,
+ "",
+ "regards,",
+ " $program\@$hostname (version $version)"
+ );
+ }
+ $last_msgs = $msgs;
+ $msgs = '';
+}
+######################################################################
+## split_by_comma
+## merge
+## default
+## minimum
+## opt
+######################################################################
+sub split_by_comma {
+ my $string = shift;
+
+ return split /\s*[, ]\s*/, $string if defined $string;
+ return ();
+}
+sub merge {
+ my %merged = ();
+ foreach my $h (@_) {
+ foreach my $k (keys %$h) {
+ $merged{$k} = $h->{$k} unless exists $merged{$k};
+ }
+ }
+ return \%merged;
+}
+sub default {
+ my $v = shift;
+ return $variables{'merged'}{$v}{'default'};
+}
+sub minimum {
+ my $v = shift;
+ 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);
+}
+sub min {
+ my $min = shift;
+ foreach my $arg (@_) {
+ $min = $arg if $arg < $min;
+ }
+ return $min;
+}
+sub max {
+ my $max = shift;
+ foreach my $arg (@_) {
+ $max = $arg if $arg > $max;
+ }
+ return $max;
+}
+######################################################################
+## ynu
+######################################################################
+sub ynu {
+ my ($value, $yes, $no, $undef) = @_;
+
+ return $no if !($value // '');
+ return $yes if $value eq '1';
+ foreach (qw(yes true)) {
+ return $yes if $_ =~ /^$value/i;
+ }
+ foreach (qw(no false)) {
+ return $no if $_ =~ /^$value/i;
+ }
+ return $undef;
+}
+######################################################################
+## msg
+## debug
+## warning
+## fatal
+######################################################################
+sub _msg {
+ my $fh = shift;
+ my $log = shift;
+ my $prefix = shift;
+ my $format = shift;
+ my $buffer = sprintf $format, @_;
+ chomp($buffer);
+
+ $prefix = sprintf "%-9s ", $prefix if $prefix;
+ if ($file) {
+ $prefix .= "file $file";
+ $prefix .= ", line $lineno" if $lineno;
+ $prefix .= ": ";
+ }
+ if ($prefix) {
+ $buffer = "$prefix$buffer";
+ $buffer =~ s/\n/\n$prefix/g;
+ }
+ $buffer .= "\n";
+ print $fh $buffer;
+
+ $msgs .= $buffer if $log;
+ logger($buffer) if $log;
+
+}
+sub msg { _msg(*STDOUT, 0, '', @_); }
+sub verbose { _msg(*STDOUT, 1, @_) if opt('verbose'); }
+sub info { _msg(*STDOUT, 1, 'INFO:', @_) if opt('verbose'); }
+sub debug { _msg(*STDOUT, 0, 'DEBUG:', @_) if opt('debug'); }
+sub debug2 { _msg(*STDOUT, 0, 'DEBUG:', @_) if opt('debug') && opt('verbose'); }
+sub warning { _msg(*STDERR, 1, 'WARNING:', @_); }
+sub fatal { _msg(*STDERR, 1, 'FATAL:', @_); sendmail(); exit(1); }
+sub success { _msg(*STDOUT, 1, 'SUCCESS:', @_); }
+sub failed { _msg(*STDERR, 1, 'FAILED:', @_); $result = 'FAILED'; }
+sub prettytime { return scalar(localtime(shift)); }
+
+sub prettyinterval {
+ my $interval = shift;
+ use integer;
+ my $s = $interval % 60; $interval /= 60;
+ my $m = $interval % 60; $interval /= 60;
+ my $h = $interval % 24; $interval /= 24;
+ my $d = $interval;
+
+ my $string = "";
+ $string .= "$d day" if $d;
+ $string .= "s" if $d > 1;
+ $string .= ", " if $string && $h;
+ $string .= "$h hour" if $h;
+ $string .= "s" if $h > 1;
+ $string .= ", " if $string && $m;
+ $string .= "$m minute" if $m;
+ $string .= "s" if $m > 1;
+ $string .= ", " if $string && $s;
+ $string .= "$s second" if $s;
+ $string .= "s" if $s > 1;
+ return $string;
+}
+sub interval {
+ my $value = shift;
+ if ($value =~ /^(\d+)(seconds|s)/i) {
+ $value = $1;
+ } elsif ($value =~ /^(\d+)(minutes|m)/i) {
+ $value = $1 * 60;
+ } elsif ($value =~ /^(\d+)(hours|h)/i) {
+ $value = $1 * 60*60;
+ } elsif ($value =~ /^(\d+)(days|d)/i) {
+ $value = $1 * 60*60*24;
+ } elsif ($value !~ /^\d+$/) {
+ $value = undef;
+ }
+ return $value;
+}
+sub interval_expired {
+ my ($host, $time, $interval) = @_;
+
+ return 1 if !exists $cache{$host};
+ return 1 if !exists $cache{$host}{$time} || !$cache{$host}{$time};
+ return 1 if !exists $config{$host}{$interval} || !$config{$host}{$interval};
+
+ return $now > ($cache{$host}{$time} + $config{$host}{$interval});
+}
+
+
+
+######################################################################
+## check_value
+######################################################################
+sub check_value {
+ my ($value, $def) = @_;
+ my $type = $def->{'type'};
+ my $min = $def->{'minimum'};
+ my $required = $def->{'required'};
+
+ if (!defined $value && !$required) {
+ ;
+
+ } 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+$/;
+ $value = $min if defined($min) && $value < $min;
+
+ } elsif ($type eq T_BOOL) {
+ if ($value =~ /^(y(es)?|t(rue)?|1)$/i) {
+ $value = 1;
+ } elsif ($value =~ /^(n(o)?|f(alse)?|0)$/i) {
+ $value = 0;
+ } else {
+ return undef;
+ }
+ } elsif ($type eq T_FQDN || $type eq T_OFQDN && $value ne '') {
+ $value = lc $value;
+ return undef if $value !~ /[^.]\.[^.]/;
+
+ } elsif ($type eq T_FQDNP) {
+ $value = lc $value;
+ return undef if $value !~ /[^.]\.[^.].*(:\d+)?$/;
+
+ } elsif ($type eq T_PROTO) {
+ $value = lc $value;
+ return undef if !exists $services{$value};
+
+ } elsif ($type eq T_USE) {
+ $value = lc $value;
+ return undef if !exists $ip_strategies{$value};
+
+ } elsif ($type eq T_USEV4) {
+ $value = lc $value;
+ return undef if ! exists $ipv4_strategies{$value};
+
+ } elsif ($type eq T_USEV6) {
+ $value = lc $value;
+ return undef if ! exists $ipv6_strategies{$value};
+
+ } elsif ($type eq T_FILE) {
+ return undef if $value eq "";
+
+ } elsif ($type eq T_IF) {
+ return undef if $value !~ /^[a-zA-Z0-9:._-]+$/;
+
+ } elsif ($type eq T_PROG) {
+ return undef if $value eq "";
+
+ } elsif ($type eq T_LOGIN) {
+ return undef if $value eq "";
+
+ } elsif ($type eq T_IP) {
+ return undef if !is_ipv4($value) && !is_ipv6($value);
+
+ } elsif ($type eq T_IPV4) {
+ return undef if !is_ipv4($value);
+
+ } elsif ($type eq T_IPV6) {
+ return undef if !is_ipv6($value);
+
+ }
+ return $value;
+}
+######################################################################
+## encode_base64 - from MIME::Base64
+######################################################################
+sub encode_base64 ($;$) {
+ my $res = '';
+ my $eol = $_[1];
+ $eol = "\n" unless defined $eol;
+ pos($_[0]) = 0; # ensure start at the beginning
+ while ($_[0] =~ /(.{1,45})/gs) {
+ $res .= substr(pack('u', $1), 1);
+ chop($res);
+ }
+ $res =~ tr|` -_|AA-Za-z0-9+/|; # `# help emacs
+
+ # fix padding at the end
+ my $padding = (3 - length($_[0]) % 3) % 3;
+ $res =~ s/.{$padding}$/'=' x $padding/e if $padding;
+ $res;
+}
+######################################################################
+## load_ssl_support
+######################################################################
+sub load_ssl_support {
+ my $ssl_loaded = eval { require IO::Socket::SSL };
+ unless ($ssl_loaded) {
+ fatal("%s", <<"EOM");
+Error loading the Perl module IO::Socket::SSL needed for SSL connect.
+On Debian, the package libio-socket-ssl-perl must be installed.
+On Red Hat, the package perl-IO-Socket-SSL must be installed.
+On Alpine, the package perl-io-socket-ssl must be installed.
+EOM
+ }
+ import IO::Socket::SSL;
+ { no warnings; $IO::Socket::SSL::DEBUG = 0; }
+}
+
+######################################################################
+## load_ipv6_support
+######################################################################
+sub load_ipv6_support {
+ my $ipv6_loaded = eval { require IO::Socket::INET6 };
+ unless ($ipv6_loaded) {
+ fatal("%s", <<"EOM");
+Error loading the Perl module IO::Socket::INET6 needed for ipv6 connect.
+On Debian, the package libio-socket-inet6-perl must be installed.
+On Red Hat, the package perl-IO-Socket-INET6 must be installed.
+On Alpine, the package perl-io-socket-inet6 must be installed.
+EOM
+ }
+ import IO::Socket::INET6;
+ { no warnings; $IO::Socket::INET6::DEBUG = 0; }
+}
+
+######################################################################
+## load_sha1_support
+######################################################################
+sub load_sha1_support {
+ my $why = shift;
+ my $sha1_loaded = eval { require Digest::SHA1 };
+ my $sha_loaded = eval { require Digest::SHA };
+ unless ($sha1_loaded || $sha_loaded) {
+ fatal("%s", <<"EOM");
+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.
+EOM
+ }
+ if ($sha1_loaded) {
+ import Digest::SHA1 (qw/sha1_hex/);
+ } elsif ($sha_loaded) {
+ import Digest::SHA (qw/sha1_hex/);
+ }
+}
+######################################################################
+## load_json_support
+######################################################################
+sub load_json_support {
+ my $why = shift;
+ my $json_loaded = eval { require JSON::PP };
+ unless ($json_loaded) {
+ fatal("%s", <<"EOM");
+Error loading the Perl module JSON::PP needed for $why update.
+EOM
+ }
+ import JSON::PP (qw/decode_json encode_json/);
+}
+
+######################################################################
+## geturl
+######################################################################
+sub geturl {
+ return opt('curl') ? fetch_via_curl(@_) : fetch_via_socket_io(@_);
+}
+
+sub fetch_via_socket_io {
+ my %params = @_;
+ my $proxy = $params{proxy};
+ my $url = $params{url};
+ my $login = $params{login};
+ my $password = $params{password};
+ my $ipversion = $params{ipversion} // '';
+ my $headers = $params{headers} // '';
+ my $method = $params{method} // 'GET';
+ my $data = $params{data} // '';
+ my ($peer, $server, $port, $default_port, $use_ssl);
+ my ($sd, $request, $reply);
+
+ ## canonify proxy and url
+ my $force_ssl;
+ $force_ssl = 1 if ($url =~ /^https:/);
+ $proxy =~ s%^https?://%%i if defined($proxy);
+ $url =~ s%^https?://%%i;
+ $server = $url;
+ $server =~ s%[?/].*%%;
+ $url =~ s%^[^?/]*/?%%;
+
+ if ($force_ssl || ($globals{'ssl'} && !($params{ignore_ssl_option} // 0))) {
+ $use_ssl = 1;
+ $default_port = '443';
+ } else {
+ $use_ssl = 0;
+ $default_port = '80';
+ }
+ debug("proxy = %s", $proxy // '');
+ debug("protocol = %s", $use_ssl ? "https" : "http");
+ debug("server = %s", $server);
+ (my $_url = $url) =~ s%\?.*%?%; #redact ALL parameters passed on URL, including possible passwords
+ debug("url = %s", $_url);
+ debug("ip ver = %s", $ipversion);
+
+ ## determine peer and port to use.
+ $peer = $proxy // $server;
+ $peer =~ s%[?/].*%%;
+ if ($peer =~ /^\[([^]]+)\](?::(\d+))?$/ || $peer =~ /^([^:]+)(?::(\d+))?/) {
+ $peer = $1;
+ $port = $2 // $default_port;
+ } else {
+ failed("unable to extract host and port from %s", $peer);
+ return undef;
+ }
+
+ $request = "$method ";
+ if (!$use_ssl) {
+ $request .= "http://$server" if defined($proxy);
+ } else {
+ $request .= "https://$server" if defined($proxy);
+ }
+ $request .= "/$url HTTP/1.1\n";
+ $request .= "Host: $server\n";
+
+ if (defined($login) || defined($password)) {
+ my $auth = encode_base64(($login // '') . ':' . ($password // ''), '');
+ $request .= "Authorization: Basic $auth\n";
+ }
+ $request .= "User-Agent: ${program}/${version}\n";
+ if ($data) {
+ $request .= "Content-Type: application/x-www-form-urlencoded\n" if $headers !~ /^Content-Type:/mi;
+ $request .= "Content-Length: " . length($data) . "\n";
+ }
+ $request .= "Connection: close\n";
+ $headers .= "\n" if $headers ne '' && substr($headers, -1) ne "\n";
+ $request .= $headers;
+ $request .= "\n";
+ # RFC 7230 says that all lines before the body must end with .
+ (my $rq = $request) =~ s/(? $peer,
+ PeerPort => $port,
+ Proto => 'tcp',
+ MultiHomed => 1,
+ Timeout => opt('timeout'),
+ );
+ my $socket_class = 'IO::Socket::INET';
+ if ($use_ssl) {
+ # IO::Socket::SSL will load IPv6 support if available on the system.
+ load_ssl_support;
+ $socket_class = 'IO::Socket::SSL';
+ $socket_args{SSL_ca_file} = opt('ssl_ca_file') if defined(opt('ssl_ca_file'));
+ $socket_args{SSL_ca_path} = opt('ssl_ca_dir') if defined(opt('ssl_ca_dir'));
+ $socket_args{SSL_verify_mode} = ($params{ssl_validate} // 1)
+ ? IO::Socket::SSL->SSL_VERIFY_PEER
+ : IO::Socket::SSL->SSL_VERIFY_NONE;
+ } elsif ($globals{'ipv6'} || $ipversion eq '6') {
+ load_ipv6_support;
+ $socket_class = 'IO::Socket::INET6';
+ }
+ if (defined($params{_testonly_socket_class})) {
+ $socket_args{original_socket_class} = $socket_class;
+ $socket_class = $params{_testonly_socket_class};
+ }
+ if ($ipversion eq '4') {
+ $socket_args{Domain} = PF_INET;
+ $socket_args{Family} = AF_INET;
+ } elsif ($ipversion eq '6') {
+ $socket_args{Domain} = PF_INET6;
+ $socket_args{Family} = AF_INET6;
+ } elsif ($ipversion ne '') {
+ fatal("geturl passed unsupported 'ipversion' value %s", $ipversion);
+ }
+
+ my $ipv = $ipversion eq '' ? '' : sprintf(" (IPv%s)", $ipversion);
+ my $peer_port_ipv = sprintf("%s:%s%s", $peer, $port, $ipv);
+ my $to = sprintf("%s%s%s", $server, defined($proxy) ? " via proxy $peer:$port" : "", $ipv);
+ verbose("CONNECT:", "%s", $to);
+ $0 = sprintf("%s - connecting to %s", $program, $peer_port_ipv);
+ if (opt('exec')) {
+ $sd = $socket_class->new(%socket_args);
+ defined($sd) or warning("cannot connect to %s socket: %s%s", $peer_port_ipv, $@,
+ $use_ssl ? ' ' . IO::Socket::SSL::errstr() : '');
+ } else {
+ debug("skipped network connection");
+ verbose("SENDING:", "%s", $request);
+ }
+ if (defined $sd) {
+ ## send the request to the http server
+ verbose("CONNECTED: ", $use_ssl ? 'using SSL' : 'using HTTP');
+ verbose("SENDING:", "%s", $request);
+
+ $0 = sprintf("%s - sending to %s", $program, $peer_port_ipv);
+ my $result = syswrite $sd, $rq;
+ if ($result != length($rq)) {
+ warning("cannot send to %s (%s).", $peer_port_ipv, $!);
+ } else {
+ $0 = sprintf("%s - reading from %s", $program, $peer_port_ipv);
+ eval {
+ local $SIG{'ALRM'} = sub { die "timeout"; };
+ alarm(opt('timeout')) if opt('timeout') > 0;
+ while ($_ = <$sd>) {
+ $0 = sprintf("%s - read from %s", $program, $peer_port_ipv);
+ verbose("RECEIVE:", "%s", $_ // "");
+ $reply .= $_ // '';
+ }
+ if (opt('timeout') > 0) {
+ alarm(0);
+ }
+ };
+ close($sd);
+
+ if ($@ and $@ =~ /timeout/) {
+ warning("TIMEOUT: %s after %s seconds", $to, opt('timeout'));
+ $reply = '';
+ }
+ $reply //= '';
+ }
+ }
+ $0 = sprintf("%s - closed %s", $program, $peer_port_ipv);
+
+ ## during testing simulate reading the URL
+ if (opt('test')) {
+ my $filename = "$server/$url";
+ $filename =~ s|/|%2F|g;
+ if (opt('exec')) {
+ $reply = save_file("$savedir/$filename", $reply, 'unique');
+ } else {
+ $reply = load_file("$savedir/$filename");
+ }
+ }
+
+ $reply =~ s/\r//g if defined $reply;
+ return $reply;
+}
+
+######################################################################
+## curl_cmd() function to execute system curl command
+######################################################################
+sub curl_cmd {
+ my @params = @_;
+ my $tmpfile;
+ my $tfh;
+ my $system_curl = quotemeta(subst_var('@CURL@', 'curl'));
+ my %curl_codes = ( ## Subset of error codes from https://curl.haxx.se/docs/manpage.html
+ 2 => "Failed to initialize. (Most likely a bug in ddclient, please open issue at https://github.com/ddclient/ddclient)",
+ 3 => "URL malformed. The syntax was not correct",
+ 5 => "Couldn't resolve proxy. The given proxy host could not be resolved.",
+ 6 => "Couldn't resolve host. The given remote host was not resolved.",
+ 7 => "Failed to connect to host.",
+ 22 => "HTTP page not retrieved. The requested url was not found or returned another error.",
+ 28 => "Operation timeout. The specified time-out period was reached according to the conditions.",
+ 35 => "SSL connect error. The SSL handshaking failed.",
+ 47 => "Too many redirects. When following redirects, curl hit the maximum amount.",
+ 52 => "The server didn't reply anything, which here is considered an error.",
+ 51 => "The peer's SSL certificate or SSH MD5 fingerprint was not OK.",
+ 58 => "Problem with the local certificate.",
+ 60 => "Peer certificate cannot be authenticated with known CA certificates.",
+ 67 => "The user name, password, or similar was not accepted and curl failed to log in.",
+ 77 => "Problem with reading the SSL CA cert (path? access rights?).",
+ 78 => "The resource referenced in the URL does not exist.",
+ 127 => "You requested network access with curl but $system_curl was not found",
+ );
+
+ debug("CURL: %s", $system_curl);
+ fatal("curl not found") if ($system_curl eq '');
+ return '' if (scalar(@params) == 0); ## no parameters provided
+
+ # Hard code to /tmp rather than use system TMPDIR to protect from malicious
+ # shell instructions in TMPDIR environment variable. All systems should have /tmp.
+ $tfh = File::Temp->new(DIR => '/tmp',
+ TEMPLATE => 'ddclient_XXXXXXXXXX');
+ $tmpfile = $tfh->filename;
+
+ debug("CURL Tempfile: %s", $tmpfile);
+ {
+ local $\ = "\n"; ## Terminate the file,
+ local $, = "\n"; ## and each parameter, with a newline.
+ print($tfh @params);
+ }
+ close($tfh);
+ my $reply = qx{ $system_curl --config $tmpfile 2>/dev/null; };
+ if ((my $rc = $?>>8) != 0) {
+ warning("CURL error (%d) %s", $rc, $curl_codes{$rc} // "Unknown return code. Check $system_curl is installed and its manpage.");
+ }
+ return $reply;
+}
+
+######################################################################
+## escape_curl_param() makes sure any special characters within a
+## curl parameter is properly escaped.
+######################################################################
+sub escape_curl_param {
+ my $str = shift // '';
+
+ return '' if ($str eq '');
+ $str =~ s/\\/\\\\/g;## Escape backslashes
+ $str =~ s/"/\\"/g; ## Escape double-quotes
+ $str =~ s/\n/\\n/g; ## Escape newline
+ $str =~ s/\r/\\r/g; ## Escape carrage return
+ $str =~ s/\t/\\t/g; ## Escape tabs
+ $str =~ s/\v/\\v/g; ## Escape vertical whitespace
+ return $str;
+}
+
+######################################################################
+## fetch_via_curl() is used for geturl() when global curl option set
+######################################################################
+sub fetch_via_curl {
+ my %params = @_;
+ my $proxy = $params{proxy};
+ my $url = $params{url};
+ my $login = $params{login};
+ my $password = $params{password};
+ my $ipversion = ($params{ipversion}) ? int($params{ipversion}) : 0;
+ my $headers = $params{headers} // '';
+ my $method = $params{method} // 'GET';
+ my $data = $params{data} // '';
+
+ my $reply;
+ my $server;
+ my $use_ssl = 0;
+ my $force_ssl = 0;
+ my $protocol;
+ my $timeout = opt('timeout');
+ my @curlopt = ();
+ my @header_lines = ();
+
+ ## canonify proxy and url
+ $force_ssl = 1 if ($url =~ /^https:/);
+ $proxy =~ s%^https?://%%i if defined($proxy);
+ $url =~ s%^https?://%%i;
+ $server = $url;
+ $server =~ s%[?/].*%%;
+ $url =~ s%^[^?/]*/?%%;
+
+ $use_ssl = 1 if ($force_ssl || ($globals{'ssl'} && !($params{ignore_ssl_option} // 0)));
+
+ $protocol = ($use_ssl ? "https" : "http");
+
+ debug("proxy = %s", $proxy // '');
+ debug("protocol = %s", $protocol);
+ debug("server = %s", $server);
+ (my $_url = $url) =~ s%\?.*%?%; #redact possible credentials
+ debug("url = %s", $_url);
+ debug("ip ver = %s", $ipversion);
+
+ if (!opt('exec')) {
+ debug("skipped network connection");
+ verbose("SENDING:", "%s", "${server}/${url}");
+ } else {
+ my $curl_loaded = eval { require WWW::Curl::Easy };
+ if ($curl_loaded) {
+ # System has the WWW::Curl::Easy module so use that
+ import WWW::Curl::Easy;
+ my $curl = WWW::Curl::Easy->new;
+
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_HEADER, 1); ## Include HTTP response for compatibility
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_SSL_VERIFYPEER, ($params{ssl_validate} // 1) ? 1 : 0 );
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_SSL_VERIFYHOST, ($params{ssl_validate} // 1) ? 1 : 0 );
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_CAINFO, opt('ssl_ca_file')) if defined(opt('ssl_ca_file'));
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_CAPATH, opt('ssl_ca_dir')) if defined(opt('ssl_ca_dir'));
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_IPRESOLVE,
+ ($ipversion == 4) ? WWW::Curl::Easy->CURL_IPRESOLVE_V4 :
+ ($ipversion == 6) ? WWW::Curl::Easy->CURL_IPRESOLVE_V6 :
+ WWW::Curl::Easy->CURL_IPRESOLVE_WHATEVER);
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_USERAGENT, "${program}/${version}");
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_CONNECTTIMEOUT, $timeout);
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_TIMEOUT, $timeout);
+
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_POST, 1) if ($method eq 'POST');
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_PUT, 1) if ($method eq 'PUT');
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_CUSTOMREQUEST, $method) if ($method ne 'GET'); ## for PATCH
+
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_USERPWD, "${login}:${password}") if (defined($login) && defined($password));
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_PROXY, "${protocol}://${proxy}") if defined($proxy);
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_URL, "${protocol}://${server}/${url}");
+
+ # Add header lines if any was provided
+ if ($headers) {
+ @header_lines = split('\n', $headers);
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_HTTPHEADER, \@header_lines);
+ }
+ # Add in the data if any was provided (for POST/PATCH)
+ if (my $datalen = length($data)) {
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_POSTFIELDS, ${data});
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_POSTFIELDSIZE, $datalen);
+ }
+ $curl->setopt(WWW::Curl::Easy->CURLOPT_WRITEDATA,\$reply);
+
+ # don't include ${url} as that might expose login credentials
+ $0 = sprintf("%s - WWW::Curl::Easy sending to %s", $program, "${protocol}://${server}");
+ verbose("SENDING:", "WWW::Curl::Easy to %s", "${protocol}://${server}");
+ verbose("SENDING:", "%s", $headers) if ($headers);
+ verbose("SENDING:", "%s", $data) if ($data);
+
+ my $rc = $curl->perform;
+
+ if ($rc != 0) {
+ warning("CURL error (%d) %s", $rc, $curl->strerror($rc));
+ debug($curl->errbuf);
+ }
+ } else {
+ # System does not have the WWW::Curl::Easy module so attempt with system Curl command
+ push(@curlopt, "silent");
+ push(@curlopt, "include"); ## Include HTTP response for compatibility
+ push(@curlopt, "insecure") if ($use_ssl && !($params{ssl_validate} // 1));
+ push(@curlopt, "cacert=\"".escape_curl_param(opt('ssl_ca_file')).'"') if defined(opt('ssl_ca_file'));
+ push(@curlopt, "capath=\"".escape_curl_param(opt('ssl_ca_dir')).'"') if defined(opt('ssl_ca_dir'));
+ push(@curlopt, "ipv4") if ($ipversion == 4);
+ push(@curlopt, "ipv6") if ($ipversion == 6);
+ push(@curlopt, "user-agent=\"".escape_curl_param("${program}/${version}").'"');
+ push(@curlopt, "connect-timeout=$timeout");
+ push(@curlopt, "max-time=$timeout");
+ push(@curlopt, "request=$method");
+ push(@curlopt, "user=\"".escape_curl_param("${login}:${password}").'"') if (defined($login) && defined($password));
+ push(@curlopt, "proxy=\"".escape_curl_param("${protocol}://${proxy}").'"') if defined($proxy);
+ push(@curlopt, "url=\"".escape_curl_param("${protocol}://${server}/${url}").'"');
+
+ # Each header line is added individually
+ @header_lines = split('\n', $headers);
+ $_ = "header=\"".escape_curl_param($_).'"' foreach (@header_lines);
+ push(@curlopt, @header_lines);
+
+ # Add in the data if any was provided (for POST/PATCH)
+ push(@curlopt, "data=\"".escape_curl_param(${data}).'"') if ($data);
+
+ # don't include ${url} as that might expose login credentials
+ $0 = sprintf("%s - Curl system cmd sending to %s", $program, "${protocol}://${server}");
+ verbose("SENDING:", "Curl system cmd to %s", "${protocol}://${server}");
+ verbose("SENDING:", "%s", $_) foreach (@curlopt);
+
+ $reply = curl_cmd(@curlopt);
+ }
+ verbose("RECEIVE:", "%s", $reply // "");
+ if (!$reply) {
+ # don't include ${url} as that might expose login credentials
+ warning("curl cannot connect to %s://%s using IPv%s",${protocol},${server},$ipversion);
+ }
+ }
+
+ ## during testing simulate reading the URL
+ if (opt('test')) {
+ my $filename = "$server/$url";
+ $filename =~ s|/|%2F|g;
+ if (opt('exec')) {
+ $reply = save_file("$savedir/$filename", $reply, 'unique');
+ } else {
+ $reply = load_file("$savedir/$filename");
+ }
+ }
+
+ $reply =~ s/\r//g if defined $reply;
+ return $reply;
+}
+
+######################################################################
+## get_ip
+######################################################################
+sub get_ip {
+ my $use = lc shift;
+ $use = 'disabled' if ($use eq 'no'); # backward compatibility
+ my $h = shift;
+ my ($ip, $arg, $reply, $url, $skip) = (undef, opt($use, $h), '');
+ $arg = '' unless $arg;
+
+ if ($use eq 'ip') {
+ $ip = opt('ip', $h);
+ if (!is_ipv4($ip) && !is_ipv6($ip)) {
+ warning("'%s' is not a valid IPv4 or IPv6 address", $ip // '');
+ $ip = undef;
+ }
+ $arg = 'ip';
+
+ } elsif ($use eq 'if') {
+ $ip = get_ip_from_interface($arg);
+
+ } elsif ($use eq 'cmd') {
+ if ($arg) {
+ $skip = opt('cmd-skip', $h) // '';
+ $reply = `$arg`;
+ $reply = '' if $?;
+ }
+
+ } elsif ($use eq 'web') {
+ $url = opt('web', $h) // '';
+ $skip = opt('web-skip', $h) // '';
+
+ if (exists $builtinweb{$url}) {
+ $skip = $builtinweb{$url}->{'skip'} unless $skip;
+ $url = $builtinweb{$url}->{'url'};
+ }
+ $arg = $url;
+
+ if ($url) {
+ $reply = geturl(
+ proxy => opt('proxy', $h),
+ url => $url,
+ ssl_validate => opt('web-ssl-validate', $h),
+ ) // '';
+ }
+
+ } elsif (($use eq 'cisco')) {
+ # Stuff added to support Cisco router ip http daemon
+ # User fw-login should only have level 1 access to prevent
+ # password theft. This is pretty harmless.
+ my $queryif = opt('if', $h);
+ $skip = opt('fw-skip', $h) // '';
+
+ # Convert slashes to protected value "\/"
+ $queryif =~ s%\/%\\\/%g;
+
+ # Protect special HTML characters (like '?')
+ $queryif =~ s/([\?&= ])/sprintf("%%%02x", ord($1))/ge;
+
+ $url = "http://" . opt('fw', $h) . "/level/1/exec/show/ip/interface/brief/${queryif}/CR";
+ $reply = geturl(
+ url => $url,
+ login => opt('fw-login', $h),
+ password => opt('fw-password', $h),
+ ignore_ssl_option => 1,
+ ssl_validate => opt('fw-ssl-validate', $h),
+ ) // '';
+ $arg = $url;
+
+ } elsif (($use eq 'cisco-asa')) {
+ # Stuff added to support Cisco ASA ip https daemon
+ # User fw-login should only have level 1 access to prevent
+ # password theft. This is pretty harmless.
+ my $queryif = opt('if', $h);
+ $skip = opt('fw-skip', $h) // '';
+
+ # Convert slashes to protected value "\/"
+ $queryif =~ s%\/%\\\/%g;
+
+ # Protect special HTML characters (like '?')
+ $queryif =~ s/([\?&= ])/sprintf("%%%02x", ord($1))/ge;
+
+ $url = "https://" . opt('fw', $h) . "/exec/show%20interface%20${queryif}";
+ $reply = geturl(
+ url => $url,
+ login => opt('fw-login', $h),
+ password => opt('fw-password', $h),
+ ignore_ssl_option => 1,
+ ssl_validate => opt('fw-ssl-validate', $h),
+ ) // '';
+ $arg = $url;
+
+ } elsif ($use eq 'disabled') {
+ ## This is a no-op... Do not get an IP address for this host/service
+ $reply = '';
+
+ } else {
+ $url = opt('fw', $h) // '';
+ $skip = opt('fw-skip', $h) // '';
+
+ if (exists $builtinfw{$use}) {
+ $skip = $builtinfw{$use}->{'skip'} unless $skip;
+ $url = "http://${url}" . $builtinfw{$use}->{'url'} unless $url =~ /\//;
+ }
+ $arg = $url;
+
+ if ($url) {
+ $reply = geturl(
+ url => $url,
+ login => opt('fw-login', $h),
+ password => opt('fw-password', $h),
+ ignore_ssl_option => 1,
+ ssl_validate => opt('fw-ssl-validate', $h),
+ ) // '';
+ }
+ }
+ if (!defined $reply) {
+ $reply = '';
+ }
+ if (($skip // '') ne '') {
+ $skip =~ s/ /\\s/is;
+ $reply =~ s/^.*?${skip}//is;
+ }
+ $ip //= extract_ipv4($reply) // extract_ipv6($reply);
+ warning("found neither IPv4 nor IPv6 address") if !defined($ip);
+ if ($use ne 'ip' && ($ip // '') eq '0.0.0.0') {
+ $ip = undef;
+ }
+
+ debug("get_ip: using %s, %s reports %s", $use, $arg, $ip // "");
+ return $ip;
+}
+
+######################################################################
+## 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 {
+ return (shift // '') =~ /\A$regex_ipv4\z/;
+}
+
+######################################################################
+## 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|_)($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:
+ (?/dev/null };
+ ## Fallback is the netstat command. This is only option on MacOS.
+ if ($?) { $cmd = "netstat -rn -$ipver"; $reply = qx{ $cmd 2>/dev/null }; } # Linux, FreeBSD
+ if ($?) { $cmd = "netstat -rn -f $ipstr"; $reply = qx{ $cmd 2>/dev/null }; } # MacOS
+ if ($?) { $cmd = "netstat -rn"; $reply = qx{ $cmd 2>/dev/null }; } # Busybox
+ if ($?) { $cmd = "missing ip or netstat command";
+ failed("Unable to obtain default route information -- %s", $cmd)
+ }
+ }
+ debug("Reply from '%s' :\n------\n%s------", $cmd, $reply);
+
+ # Check we have IPv6 address in case we got routing table from non-specific cmd above
+ return undef if (($ipver == 6) && !extract_ipv6($reply));
+ # Filter down to just the default interfaces
+ my @list = split(/\n/, $reply);
+ @list = grep(/^default|^(?:0\.){3}0|^::\/0/, @list); # Select 'default' or '0.0.0.0' or '::/0'
+ return undef if (scalar(@list) == 0);
+ debug("Default routes found for IPv%s :\n%s", $ipver, join("\n",@list));
+
+ # now check each interface to make sure it is global (not loopback).
+ foreach my $line (@list) {
+ ## Interface will be after "dev" or the last word in the line. Must accept blank spaces
+ ## at the end. Interface name may not have any whitespace or forward slash.
+ $line =~ /\bdev\b\s*\K[^\s\/]+|\b[^\s\/]+(?=[\s\/]*$)/;
+ my $interface = $&;
+ ## If test data was passed in skip following tests
+ if ($cmd ne "test") {
+ ## We do not want the loopback interface or anything interface without global scope
+ $cmd = "ip -$ipver -o addr show dev $interface scope global"; $reply = qx{$cmd 2>/dev/null};
+ if ($?) { $cmd = "ifconfig $interface"; $reply = qx{$cmd 2>/dev/null}; }
+ if ($?) { $cmd = "missing ip or ifconfig command";
+ failed("Unable to obtain information for '%s' -- %s", $interface, $cmd);
+ }
+ debug("Reply from '%s' :\n------\n%s------", $cmd, $reply);
+ }
+ ## Has global scope, is not LOOPBACK
+ return($interface) if (($reply) && ($reply !~ /\bLOOPBACK\b/));
+ }
+ return undef;
+}
+
+######################################################################
+## get_ip_from_interface() finds an IPv4 or IPv6 address from a network
+## interface. Defaults to IPv4 unless '6' passed as 2nd parameter.
+######################################################################
+sub get_ip_from_interface {
+ my $interface = shift // "default";
+ my $ipver = int(shift // 4); ## Defaults to IPv4 if not specified
+ my $scope = lc(shift // "gua"); ## "gua" or "ula"
+ my $reply = shift // ''; ## Pass in data for unit testing purposes only
+ my $MacOS = shift // 0; ## For testing can set to 1 if input data is MacOS/FreeBSD format
+ my $count = 0;
+ my $cmd = "test";
+
+ if (($ipver != 4) && ($ipver != 6)) {
+ warning("get_ip_from_interface() invalid IP version: %s", $ipver);
+ return undef;
+ }
+
+ if ((lc($interface) eq "default") && (!$reply)) { ## skip if test data passed in.
+ $interface = get_default_interface($ipver);
+ return undef if !defined($interface);
+ }
+
+ if ($ipver == 4) {
+ if (!$reply) { ## skip if test data passed in.
+ ## Try ip first, then ifconfig.
+ $cmd = "ip -4 -o addr show dev $interface scope global"; $reply = qx{$cmd 2>/dev/null};
+ if ($?) { $cmd = "ifconfig $interface"; $reply = qx{$cmd 2>/dev/null}; }
+ if ($?) { $cmd = "missing ip or ifconfig command";
+ failed("Unable to obtain information for '%s' -- %s", $interface, $cmd);
+ }
+ }
+ debug("Reply from '%s' :\n------\n%s------", $cmd, $reply);
+
+ ## IPv4 is simple, we just need to find the first IPv4 address returned in the list.
+ my @reply = split(/\n/, $reply);
+ @reply = grep(/\binet\b/, @reply); # Select only IPv4 entries
+ return extract_ipv4($reply[0]);
+ }
+
+ ## From this point on we only looking for IPv6 address.
+ if (($scope ne "gua") && ($scope ne "ula")) {
+ warning("get_ip_from_interface() invalid IPv6 scope: %s, using type GUA", $scope);
+ $scope = "gua";
+ }
+
+ $cmd = "test data";
+ if (!$reply) { ## skip if test data passed in.
+ ## Try ip first, then ifconfig with -L for MacOS/FreeBSD then finally ifconfig for everything else
+ $cmd = "ip -6 -o addr show dev $interface scope global"; $reply = qx{$cmd 2>/dev/null}; # Linux
+ if ($?) { $cmd = "ifconfig -L $interface"; $MacOS = 1; $reply = qx{$cmd 2>/dev/null}; } # MacOS/FreeBSD
+ if ($?) { $cmd = "ifconfig $interface"; $reply = qx{$cmd 2>/dev/null}; } # Anything without iproute2 or -L
+ if ($?) { $cmd = "missing ip or ifconfig command";
+ failed("Unable to obtain information for '%s' -- %s", $interface, $cmd);
+ }
+ }
+ debug("Reply from '%s' :\n------\n%s------", $cmd, $reply);
+
+ ## IPv6 is more complex than IPv4. Start by filtering on only "inet6" addresses
+ ## Then remove deprecated or temporary addresses and finally seleect on global or local addresses
+ my @reply = split(/\n/, $reply);
+ @reply = grep(/\binet6\b/, @reply); # Select only IPv6 entries
+ @reply = grep(!/\bdeprecated\b|\btemporary\b/, @reply); # Remove deprecated and temporary
+ @reply = ($scope eq "gua") ? grep(/$regex_ipv6_global/, @reply) # Select only global addresses
+ : grep(/$regex_ipv6_ula/, @reply); # or only ULA addresses
+ debug("Raw IPv6 after filtering for %s addresses %s: (%s)\r\n%s", uc($scope), $interface, scalar(@reply), join("\n", @reply));
+
+ ## If we filter down to zero or one result then we are done...
+ return undef if (($count = scalar(@reply)) == 0);
+ return extract_ipv6($reply[0]) if ($count == 1);
+
+ ## If there are more than one we need to select the "best".
+ ## First choice would be a static address.
+ my @static = ($MacOS == 1) ? grep(!/^.*\bvltime\b.*$/i, @reply) # MacOS/FreeBSD, no 'vltime'
+ : grep(/^.*\bvalid_lft.\bforever\b.*$/i, @reply); # Everything else 'forever' life
+ $count = scalar(@static);
+ debug("Possible Static IP addresses %s: (%s)\r\n%s", $interface, $count, join("\n", @static));
+
+ ## If only one result then we are done. If there are more than one static addresses
+ ## then we will replace our original list with the list of statics and sort on them.
+ ## If zero static addresses we fall through with our original list.
+ return extract_ipv6($static[0]) if ($count == 1);
+ @reply = @static if ($count > 1);
+
+ ## Sort what we have by the prefix length, IP address "length" and finally valid life.
+ my @sorted = sort {
+ ## We give preference to IP addressess with the longest prefix... so we prefer a /128 over a /64
+ ## this is a decimal (\d+) either after the word "prefixlen" or after a forward slash.
+ (($b =~ /(?:\bprefixlen\b\s*|\/)(\d+)/i)[0] // 0) <=> (($a =~ /(?:\bprefixlen\b\s*|\/)(\d+)/i)[0] // 0)
+
+ ## If there are multiple the same then we prefer "shorter" IP addresses in the
+ ## theory that a shorter address is more likely assigned by DHCPv6 than SLAAC.
+ ## E.g. 2001:db8:4341:0781::8214/64 is preferable to 2001:db8:4341:0781:34a6:c329:c52e:8ba6/64
+ ## So we count the number () of groups of [0-9a-f] blocks in the IP address.
+ || (()= (extract_ipv6($a) // '') =~ /[0-9A-F]+/gi) <=> (()= (extract_ipv6($b) // '') =~ /[0-9A-F]+/gi)
+
+ ## Finally we check remaining valid lifetime and prefer longer remaining life.
+ ## This is a desimal (\d+) after the word "valid_lft" or "vltime". Only available
+ ## from iproute2 or MacOS/FreeBSD version of ifconfig (-L parameter).
+ || (($b =~ /(?:\bvalid_lft\b\s*|\bvltime\b\s*)(\d+)/i)[0] // 0) <=> (($a =~ /(?:\bvalid_lft\b\s*|\bvltime\b\s*)(\d+)/i)[0] // 0)
+ } @reply;
+ debug("Sorted list of IP addresss for %s: (%s)\r\n%s", $interface, scalar(@sorted), join("\n", @sorted));
+
+ ## Whatever sorted to the top is the best choice for IPv6 address
+ return extract_ipv6($sorted[0]);
+}
+
+######################################################################
+## get_ipv4
+######################################################################
+sub get_ipv4 {
+ my $usev4 = lc(shift); ## Method to obtain IP address
+ my $h = shift; ## Host/service making the request
+
+ my $ipv4 = undef; ## Found IPv4 address
+ my $reply = ''; ## Text returned from various methods
+ my $url = ''; ## URL of website or firewall
+ my $skip = ''; ## Regex of pattern to skip before looking for IP
+ my $arg = opt($usev4, $h) // ''; ## Value assigned to the "usev4" method
+
+ if ($usev4 eq 'ipv4') {
+ ## Static IPv4 address is provided in "ipv4="
+ $ipv4 = $arg;
+ if (!is_ipv4($ipv4)) {
+ warning("'%s' is not a valid IPv4",$ipv4 // '');
+ $ipv4 = undef;
+ }
+ $arg = 'ipv4'; # For debug message at end of function
+
+ } elsif ($usev4 eq 'ifv4') {
+ ## Obtain IPv4 address from interface mamed in "ifv4="
+ warning("'if-skip' is deprecated and does nothing for IPv4") if (opt('verbose') && opt('if-skip', $h));
+ $ipv4 = get_ip_from_interface($arg,4);
+
+ } elsif ($usev4 eq 'cmdv4') {
+ ## Obtain IPv4 address by executing the command in "cmdv4="
+ warning("'cmd-skip' is deprecated and does nothing for IPv4") if (opt('verbose') && opt('cmd-skip', $h));
+ if ($arg) {
+ my $sys_cmd = quotemeta($arg);
+ $reply = qx{$sys_cmd};
+ $reply = '' if $?;
+ }
+
+ } elsif ($usev4 eq 'webv4') {
+ ## Obtain IPv4 address by accessing website at url in "webv4="
+ $url = $arg;
+ $skip = opt('webv4-skip', $h) // '';
+ if (exists $builtinweb{$url}) {
+ $skip = $builtinweb{$url}->{'skip'} unless $skip;
+ $url = $builtinweb{$url}->{'url'};
+ $arg = $url;
+ }
+ if ($url) {
+ $reply = geturl( proxy => opt('proxy', $h),
+ url => $url,
+ ipversion => 4, # when using a URL to find IPv4 address we should force use of IPv4
+ ssl_validate => opt('ssl-validate', $h),
+ ) // '';
+ }
+
+ } elsif ($usev4 eq 'cisco' || $usev4 eq 'cisco-asa') {
+ # Stuff added to support Cisco router ip http or ASA https daemon
+ # User fw-login should only have level 1 access to prevent
+ # password theft. This is pretty harmless.
+ warning("'if' does nothing for IPv4. Use 'ifv4'") if (opt('if', $h));
+ warning("'fw' does nothing for IPv4. Use 'fwv4'") if (opt('fw', $h));
+ warning("'fw-skip' does nothing for IPv4. Use 'fwv4-skip'") if (opt('fw-skip', $h));
+ my $queryif = opt('ifv4', $h) // opt('if', $h);
+ $skip = opt('fwv4-skip', $h) // opt('fw-skip', $h) // '';
+ # Convert slashes to protected value "\/"
+ $queryif =~ s%\/%\\\/%g;
+ # Protect special HTML characters (like '?')
+ $queryif =~ s/([\?&= ])/sprintf("%%%02x", ord($1))/ge;
+ if ($usev4 eq 'cisco') {
+ $url = "http://" . (opt('fwv4', $h) // opt('fw', $h)) . "/level/1/exec/show/ip/interface/brief/${queryif}/CR";
+ } else {
+ $url = "https://" . (opt('fwv4', $h) // opt('fw', $h)) . "/exec/show%20interface%20${queryif}";
+ }
+ $arg = $url;
+ $reply = geturl(
+ url => $url,
+ login => opt('fw-login', $h),
+ password => opt('fw-password', $h),
+ ipversion => 4, # when using a URL to find IPv4 address we should force use of IPv4
+ ignore_ssl_option => 1,
+ ssl_validate => opt('ssl-validate', $h),
+ ) // '';
+
+ } elsif ($usev4 eq 'disabled') {
+ ## This is a no-op... Do not get an IPv4 address for this host/service
+ $reply = '';
+
+ } else {
+ warning("'fw' does nothing for IPv4. Use 'fwv4'") if (opt('fw', $h));
+ warning("'fw-skip' does nothing for IPv4. Use 'fwv4-skip'") if (opt('fw-skip', $h));
+ $url = opt('fwv4', $h) // opt('fw', $h) // '';
+ $skip = opt('fwv4-skip', $h) // opt('fw-skip', $h) // '';
+
+ if (exists $builtinfw{$usev4}) {
+ $skip = $builtinfw{$usev4}->{'skip'} unless $skip;
+ $url = "http://${url}" . $builtinfw{$usev4}->{'url'} unless $url =~ /\//;
+ }
+ $arg = $url;
+ if ($url) {
+ $reply = geturl(
+ url => $url,
+ login => opt('fw-login', $h),
+ password => opt('fw-password', $h),
+ ipversion => 4, # when using a URL to find IPv4 address we should force use of IPv4
+ ignore_ssl_option => 1,
+ ssl_validate => opt('ssl-validate', $h),
+ ) // '';
+ }
+ }
+
+ ## Set to loopback address if no text set yet
+ $reply = '0.0.0.0' if !defined($reply);
+ if (($skip // '') ne '') {
+ $skip =~ s/ /\\s/is;
+ $reply =~ s/^.*?${skip}//is;
+ }
+ ## If $ipv4 not set yet look for IPv4 address in the $reply text
+ $ipv4 //= extract_ipv4($reply);
+ ## Return undef for loopback address unless statically assigned by "ipv4=0.0.0.0"
+ $ipv4 = undef if (($usev4 ne 'ipv4') && (($ipv4 // '') eq '0.0.0.0'));
+ debug("get_ipv4: using (%s, %s) reports %s", $usev4, $arg, $ipv4 // "");
+ return $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 $ipv6 = undef; ## Found IPv6 address
+ my $reply = ''; ## Text returned from various methods
+ my $url = ''; ## URL of website or firewall
+ my $skip = ''; ## Regex of pattern to skip before looking for IP
+ my $arg = opt($usev6, $h) // ''; ## Value assigned to the "usev6" method
+
+ if ($usev6 eq 'ipv6' || $usev6 eq 'ip') {
+ ## Static IPv6 address is provided in "ipv6="
+ if ($usev6 eq 'ip') {
+ warning("'usev6=ip' is deprecated. Use 'usev6=ipv6'");
+ $usev6 = 'ipv6';
+ ## If there is a value for ipv6= use that, else use value for ip=
+ $arg = opt($usev6, $h) // $arg;
+ }
+ $ipv6 = $arg;
+ if (!is_ipv6($ipv6)) {
+ warning("'%s' is not a valid IPv6",$ipv6 // '');
+ $ipv6 = undef;
+ }
+ $arg = 'ipv6'; # For debug message at end of function
+
+ } elsif ($usev6 eq 'ifv6' || $usev6 eq 'if' ) {
+ ## Obtain IPv6 address from interface mamed in "ifv6="
+ if ($usev6 eq 'if') {
+ warning("'usev6=if' is deprecated. Use 'usev6=ifv6'");
+ $usev6 = 'ifv6';
+ ## If there is a value for ifv6= use that, else use value for if=
+ $arg = opt($usev6, $h) // $arg;
+ }
+ warning("'if-skip' is deprecated and does nothing for IPv6") if (opt('verbose') && opt('if-skip', $h));
+ $ipv6 = get_ip_from_interface($arg,6);
+
+ } elsif ($usev6 eq 'cmdv6' || $usev6 eq 'cmd') {
+ ## Obtain IPv6 address by executing the command in "cmdv6="
+ if ($usev6 eq 'cmd') {
+ warning("'usev6=cmd' is deprecated. Use 'usev6=cmdv6'");
+ $usev6 = 'cmdv6';
+ ## If there is a value for cmdv6= use that, else use value for cmd=
+ $arg = opt($usev6, $h) // $arg;
+ }
+ warning("'cmd-skip' is deprecated and does nothing for IPv6") if (opt('verbose') && opt('cmd-skip', $h));
+ if ($arg) {
+ my $sys_cmd = quotemeta($arg);
+ $reply = qx{$sys_cmd};
+ $reply = '' if $?;
+ }
+
+ } elsif ($usev6 eq 'webv6' || $usev6 eq 'web') {
+ ## Obtain IPv6 address by accessing website at url in "webv6="
+ if ($usev6 eq 'web') {
+ warning("'usev6=web' is deprecated. Use 'usev6=webv6'");
+ $usev6 = 'webv6';
+ ## If there is a value for webv6= use that, else use value for web=
+ $arg = opt($usev6, $h) // $arg;
+ }
+ warning("'web-skip' does nothing for IPv6. Use 'webv6-skip'") if (opt('web-skip', $h));
+ $url = $arg;
+ $skip = opt('webv6-skip', $h) // '';
+ if (exists $builtinweb{$url}) {
+ $skip = $builtinweb{$url}->{'skip'} unless $skip;
+ $url = $builtinweb{$url}->{'url'};
+ $arg = $url;
+ }
+ if ($url) {
+ $reply = geturl(
+ proxy => opt('proxy'),
+ url => $url,
+ ipversion => 6, # when using a URL to find IPv6 address we should force use of IPv6
+ ssl_validate => opt('ssl-validate', $h),
+ ) // '';
+ }
+
+ } elsif ($usev6 eq 'cisco' || $usev6 eq 'cisco-asa') {
+ warning("'usev6=cisco' and 'usev6=cisco-asa' are not implemented and do nothing");
+ $reply = '';
+
+ } elsif ($usev6 eq 'disabled') {
+ ## This is a no-op... Do not get an IPv6 address for this host/service
+ warning("'usev6=no' is deprecated. Use 'usev6=disabled'") if ($usev6 eq 'no');
+ $reply = '';
+
+ } else {
+ warning("'usev6=%s' is not implemented and does nothing", $usev6);
+ $reply = '';
+
+ }
+
+ ## Set to loopback address if no text set yet
+ $reply = '::' if !defined($reply);
+ if (($skip // '') ne '') {
+ $skip =~ s/ /\\s/is;
+ $reply =~ s/^.*?${skip}//is;
+ }
+ ## If $ipv6 not set yet look for IPv6 address in the $reply text
+ $ipv6 //= extract_ipv6($reply);
+ ## Return undef for loopback address unless statically assigned by "ipv6=::"
+ $ipv6 = undef if (($usev6 ne 'ipv6') && (($ipv6 // '') eq '::'));
+ debug("get_ipv6: using (%s, %s) reports %s", $usev6, $arg, $ipv6 // "");
+ return $ipv6;
+}
+
+######################################################################
+## group_hosts_by
+######################################################################
+sub group_hosts_by {
+##TODO - Update for wantipv4 and wantipv6
+ my ($hosts, $attributes) = @_;
+ my %attrs = (map({ ($_ => 1) } @$attributes), 'wantip' => 1);
+ my @attrs = sort(keys(%attrs));
+ my %groups = ();
+ foreach my $h (@$hosts) {
+ my $sig = join(',', map({ sprintf("%s=%s", $_, $config{$h}{$_} // '') } @attrs));
+ push @{$groups{$sig}}, $h;
+ }
+ 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
+######################################################################
+sub nic_examples {
+ my $examples = "";
+ my $separator = "";
+ foreach my $s (sort keys %services) {
+ my $subr = $services{$s}{'examples'};
+ my $example;
+
+ if (defined($subr) && ($example = &$subr())) {
+ chomp($example);
+ $examples .= $example;
+ $examples .= "\n\n$separator";
+ $separator = "\n";
+ }
+ }
+ my $intro = <<"EoEXAMPLE";
+== CONFIGURING ${program}
+
+The configuration file, ${program}.conf, can be used to define the
+default behaviour and operation of ${program}. The file consists of
+sequences of global variable definitions and host definitions.
+
+Global definitions look like:
+ name=value [,name=value]*
+
+For example:
+ daemon=5m
+ use=if, if=eth0
+ proxy=proxy.myisp.com
+ protocol=dyndns2
+
+specifies that ${program} should operate as a daemon, checking the
+eth0 interface for an IP address change every 5 minutes and use the
+'dyndns2' protocol by default. The daemon interval can be specified
+as seconds (600s), minutes (5m), hours (1h) or days (1d).
+
+Host definitions look like:
+ [name=value [,name=value]*]* a.host.domain [,b.host.domain] [login] [password]
+
+For example:
+ protocol=noip, \\
+ login=your-username, password=your-password myhost.noip.com
+ login=your-username, password=your-password myhost.noip.com,myhost2.noip.com
+
+specifies two host definitions.
+
+The first definition will use the noip protocol,
+your-username and your-password to update the ip-address of
+myhost.noip.com and my2ndhost.noip.com.
+
+The second host definition will use the current default protocol
+('dyndns2'), my-login and my-password to update the ip-address of
+myhost.dyndns.org and my2ndhost.dyndns.org.
+
+The order of this sequence is significant because the values of any
+global variable definitions are bound to a host definition when the
+host definition is encountered.
+
+See the sample-${program}.conf file for further examples.
+EoEXAMPLE
+ $intro .= "\n== NIC specific variables and examples:\n$examples" if $examples;
+ return $intro;
+}
+######################################################################
+## nic_updateable
+## Returns true if we can go ahead and update the IP address at server
+######################################################################
+sub nic_updateable {
+ my $host = shift;
+ my $sub = shift;
+ 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
+
+ # If we have a valid IP address and we have previously warned that it was invalid.
+ # reset the warning count back to zero.
+ if (($use ne 'disabled') && $ip && $warned_ip{$host}) {
+ $warned_ip{$host} = 0;
+ warning("IP address for %s valid: %s. Reset warning count", $host, $ip);
+ }
+ if (($usev4 ne 'disabled') && $ipv4 && $warned_ipv4{$host}) {
+ $warned_ipv4{$host} = 0;
+ warning("IPv4 address for %s valid: %s. Reset warning count", $host, $ipv4);
+ }
+ if (($usev6 ne 'disabled') && $ipv6 && $warned_ipv6{$host}) {
+ $warned_ipv6{$host} = 0;
+ warning("IPv6 address for %s valid: %s. Reset warning count", $host, $ipv6);
+ }
+
+ if ($config{$host}{'login'} eq '') {
+ warning("null login name specified for host %s.", $host);
+
+ } elsif ($config{$host}{'password'} eq '') {
+ warning("null password specified for host %s.", $host);
+
+ } elsif ($opt{'force'}) {
+ info("forcing update of %s.", $host);
+ $update = 1;
+
+ } elsif (!exists($cache{$host})) {
+ info("forcing updating %s because no cached entry exists.", $host);
+ $update = 1;
+
+ } elsif ($cache{$host}{'wtime'} && $cache{$host}{'wtime'} > $now) {
+ warning("cannot update %s from %s to %s until after %s.",
+ $host,
+ ($cache{$host}{'ip'} ? $cache{$host}{'ip'} : ''), $ip,
+ prettytime($cache{$host}{'wtime'})
+ );
+
+ } elsif ($cache{$host}{'mtime'} && interval_expired($host, 'mtime', 'max-interval')) {
+ warning("forcing update of %s from %s to %s; %s since last update on %s.",
+ $host,
+ ($cache{$host}{'ip'} ? $cache{$host}{'ip'} : ''), $ip,
+ prettyinterval($config{$host}{'max-interval'}),
+ prettytime($cache{$host}{'mtime'})
+ );
+ $update = 1;
+
+ } elsif ( ($use ne 'disabled')
+ && ((!exists($cache{$host}{'ip'})) || ("$cache{$host}{'ip'}" ne "$ip"))) {
+ ## Check whether to update IP address for the "use" method"
+ if (($cache{$host}{'status'} eq 'good') &&
+ !interval_expired($host, 'mtime', 'min-interval')) {
+
+ warning("skipping update of %s from %s to %s.\nlast updated %s.\nWait at least %s between update attempts.",
+ $host,
+ ($cache{$host}{'ip'} ? $cache{$host}{'ip'} : ''),
+ $ip,
+ ($cache{$host}{'mtime'} ? prettytime($cache{$host}{'mtime'}) : ''),
+ prettyinterval($config{$host}{'min-interval'})
+ )
+ if opt('verbose') || !($cache{$host}{'warned-min-interval'} // 0);
+
+ $cache{$host}{'warned-min-interval'} = $now;
+
+ } elsif (($cache{$host}{'status'} ne 'good') &&
+ !interval_expired($host, 'atime', 'min-error-interval')) {
+
+ if ( opt('verbose')
+ || ( ! $cache{$host}{'warned-min-error-interval'}
+ && (($warned_ip{$host} // 0) < $inv_ip_warn_count)) ) {
+
+ warning("skipping update of %s from %s to %s.\nlast updated %s but last attempt on %s failed.\nWait at least %s between update attempts.",
+ $host,
+ ($cache{$host}{'ip'} ? $cache{$host}{'ip'} : ''),
+ $ip,
+ ($cache{$host}{'mtime'} ? prettytime($cache{$host}{'mtime'}) : ''),
+ ($cache{$host}{'atime'} ? prettytime($cache{$host}{'atime'}) : ''),
+ prettyinterval($config{$host}{'min-error-interval'})
+ );
+ if (!$ip && !opt('verbose')) {
+ $warned_ip{$host} = ($warned_ip{$host} // 0) + 1;
+ warning("IP address for %s undefined. Warned %s times, suppressing further warnings", $host, $inv_ip_warn_count)
+ if ($warned_ip{$host} >= $inv_ip_warn_count);
+ }
+ }
+
+ $cache{$host}{'warned-min-error-interval'} = $now;
+
+ } else {
+ $update = 1;
+ }
+
+ } elsif ( ($usev4 ne 'disabled')
+ && ((!exists($cache{$host}{'ipv4'})) || ("$cache{$host}{'ipv4'}" ne "$ipv4"))) {
+ ## Check whether to update IPv4 address for the "usev4" method"
+ if (($cache{$host}{'status-ipv4'} eq 'good') &&
+ !interval_expired($host, 'mtime', 'min-interval')) {
+
+ warning("skipping update of %s from %s to %s.\nlast updated %s.\nWait at least %s between update attempts.",
+ $host,
+ ($cache{$host}{'ipv4'} ? $cache{$host}{'ipv4'} : ''),
+ $ipv4,
+ ($cache{$host}{'mtime'} ? prettytime($cache{$host}{'mtime'}) : ''),
+ prettyinterval($config{$host}{'min-interval'})
+ )
+ if opt('verbose') || !($cache{$host}{'warned-min-interval'} // 0);
+
+ $cache{$host}{'warned-min-interval'} = $now;
+
+ } elsif (($cache{$host}{'status-ipv4'} ne 'good') &&
+ !interval_expired($host, 'atime', 'min-error-interval')) {
+
+ if ( opt('verbose')
+ || ( ! $cache{$host}{'warned-min-error-interval'}
+ && (($warned_ipv4{$host} // 0) < $inv_ip_warn_count)) ) {
+
+ warning("skipping update of %s from %s to %s.\nlast updated %s but last attempt on %s failed.\nWait at least %s between update attempts.",
+ $host,
+ ($cache{$host}{'ipv4'} ? $cache{$host}{'ipv4'} : ''),
+ $ipv4,
+ ($cache{$host}{'mtime'} ? prettytime($cache{$host}{'mtime'}) : ''),
+ ($cache{$host}{'atime'} ? prettytime($cache{$host}{'atime'}) : ''),
+ prettyinterval($config{$host}{'min-error-interval'})
+ );
+ if (!$ipv4 && !opt('verbose')) {
+ $warned_ipv4{$host} = ($warned_ipv4{$host} // 0) + 1;
+ warning("IPv4 address for %s undefined. Warned %s times, suppressing further warnings", $host, $inv_ip_warn_count)
+ if ($warned_ipv4{$host} >= $inv_ip_warn_count);
+ }
+ }
+
+ $cache{$host}{'warned-min-error-interval'} = $now;
+
+ } else {
+ $update = 1;
+ }
+
+ } elsif ( ($usev6 ne 'disabled')
+ && ((!exists($cache{$host}{'ipv6'})) || ("$cache{$host}{'ipv6'}" ne "$ipv6"))) {
+ ## Check whether to update IPv6 address for the "usev6" method"
+ if (($cache{$host}{'status-ipv6'} eq 'good') &&
+ !interval_expired($host, 'mtime', 'min-interval')) {
+
+ warning("skipping update of %s from %s to %s.\nlast updated %s.\nWait at least %s between update attempts.",
+ $host,
+ ($cache{$host}{'ipv6'} ? $cache{$host}{'ipv6'} : ''),
+ $ipv6,
+ ($cache{$host}{'mtime'} ? prettytime($cache{$host}{'mtime'}) : ''),
+ prettyinterval($config{$host}{'min-interval'})
+ )
+ if opt('verbose') || !($cache{$host}{'warned-min-interval'} // 0);
+
+ $cache{$host}{'warned-min-interval'} = $now;
+
+ } elsif (($cache{$host}{'status-ipv6'} ne 'good') &&
+ !interval_expired($host, 'atime', 'min-error-interval')) {
+
+ if ( opt('verbose')
+ || ( ! $cache{$host}{'warned-min-error-interval'}
+ && (($warned_ipv6{$host} // 0) < $inv_ip_warn_count)) ) {
+
+ warning("skipping update of %s from %s to %s.\nlast updated %s but last attempt on %s failed.\nWait at least %s between update attempts.",
+ $host,
+ ($cache{$host}{'ipv6'} ? $cache{$host}{'ipv6'} : ''),
+ $ipv6,
+ ($cache{$host}{'mtime'} ? prettytime($cache{$host}{'mtime'}) : ''),
+ ($cache{$host}{'atime'} ? prettytime($cache{$host}{'atime'}) : ''),
+ prettyinterval($config{$host}{'min-error-interval'})
+ );
+ if (!$ipv6 && !opt('verbose')) {
+ $warned_ipv6{$host} = ($warned_ipv6{$host} // 0) + 1;
+ warning("IPv6 address for %s undefined. Warned %s times, suppressing further warnings", $host, $inv_ip_warn_count)
+ if ($warned_ipv6{$host} >= $inv_ip_warn_count);
+ }
+ }
+
+ $cache{$host}{'warned-min-error-interval'} = $now;
+
+ } else {
+ $update = 1;
+ }
+
+ } elsif (defined($sub) && &$sub($host)) {
+ $update = 1;
+ } elsif ((defined($cache{$host}{'static'}) && defined($config{$host}{'static'}) &&
+ ($cache{$host}{'static'} ne $config{$host}{'static'})) ||
+ (defined($cache{$host}{'wildcard'}) && defined($config{$host}{'wildcard'}) &&
+ ($cache{$host}{'wildcard'} ne $config{$host}{'wildcard'})) ||
+ (defined($cache{$host}{'mx'}) && defined($config{$host}{'mx'}) &&
+ ($cache{$host}{'mx'} ne $config{$host}{'mx'})) ||
+ (defined($cache{$host}{'backupmx'}) && defined($config{$host}{'backupmx'}) &&
+ ($cache{$host}{'backupmx'} ne $config{$host}{'backupmx'}))) {
+ info("updating %s because host settings have been changed.", $host);
+ $update = 1;
+
+ } else {
+ if (opt('verbose')) {
+ if ($use ne 'disabled') {
+ success("%s: skipped: IP address was already set to %s.", $host, $ip);
+ }
+ if ($usev4 ne 'disabled') {
+ success("%s: skipped: IPv4 address was already set to %s.", $host, $ipv6);
+ }
+ if ($usev6 ne 'disabled') {
+ success("%s: skipped: IPv6 address was already set to %s.", $host, $ipv6);
+ }
+ }
+ }
+
+ $config{$host}{'status'} = $cache{$host}{'status'} // '';
+ $config{$host}{'status-ipv4'} = $cache{$host}{'status-ipv4'} // '';
+ $config{$host}{'status-ipv6'} = $cache{$host}{'status-ipv6'} // '';
+ $config{$host}{'update'} = $update;
+ if ($update) {
+ $config{$host}{'status'} = 'noconnect';
+ $config{$host}{'status-ipv4'} = 'noconnect';
+ $config{$host}{'status-ipv6'} = 'noconnect';
+ $config{$host}{'atime'} = $now;
+ $config{$host}{'wtime'} = 0;
+ $config{$host}{'warned-min-interval'} = 0;
+ $config{$host}{'warned-min-error-interval'} = 0;
+
+ delete $cache{$host}{'warned-min-interval'};
+ delete $cache{$host}{'warned-min-error-interval'};
+ }
+
+ return $update;
+}
+
+######################################################################
+## header_ok
+######################################################################
+sub header_ok {
+ my ($host, $line) = @_;
+ my $ok = 0;
+
+ if ($line =~ m%^s*HTTP/.*\s+(\d+)%i) {
+ my $result = $1;
+
+ if ($result =~ m/^2\d\d$/) {
+ $ok = 1;
+
+ } elsif ($result eq '401') {
+ failed("updating %s: authorization failed (%s)", $host, $line);
+ }
+
+ } else {
+ failed("updating %s: unexpected line (%s)", $host, $line);
+ }
+ return $ok;
+}
+######################################################################
+## nic_dyndns1_examples
+######################################################################
+sub nic_dyndns1_examples {
+ return <<"EoEXAMPLE";
+o 'dyndns1'
+
+The 'dyndns1' protocol is a deprecated protocol used by the free dynamic
+DNS service offered by www.dyndns.org. The 'dyndns2' should be used to
+update the www.dyndns.org service. However, other services are also
+using this protocol so support is still provided by ${program}.
+
+Configuration variables applicable to the 'dyndns1' protocol are:
+ protocol=dyndns1 ##
+ server=fqdn.of.service ## defaults to members.dyndns.org
+ backupmx=no|yes ## indicates that this host is the primary MX for the domain.
+ mx=any.host.domain ## a host MX'ing for this host definition.
+ wildcard=no|yes ## add a DNS wildcard CNAME record that points to {host}
+ login=service-login ## login name and password registered with the service
+ password=service-password ##
+ fully.qualified.host ## the host registered with the service.
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=dyndns1, \\
+ login=my-dyndns.org-login, \\
+ password=my-dyndns.org-password \\
+ myhost.dyndns.org
+
+ ## multiple host update with wildcard'ing mx, and backupmx
+ protocol=dyndns1, \\
+ login=my-dyndns.org-login, \\
+ password=my-dyndns.org-password, \\
+ mx=a.host.willing.to.mx.for.me,backupmx=yes,wildcard=yes \\
+ myhost.dyndns.org,my2ndhost.dyndns.org
+EoEXAMPLE
+}
+######################################################################
+## nic_dyndns1_update
+######################################################################
+sub nic_dyndns1_update {
+ debug("\nnic_dyndns1_update -------------------");
+ ## update each configured host
+ foreach my $h (@_) {
+ my $ip = delete $config{$h}{'wantip'};
+ info("setting IP address to %s for %s", $ip, $h);
+ verbose("UPDATE:", "updating %s", $h);
+
+ my $url;
+ $url = "https://$config{$h}{'server'}/nic/";
+ $url .= ynu($config{$h}{'static'}, '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');
+ }
+
+ my $reply = geturl(
+ proxy => opt('proxy'),
+ url => $url,
+ login => $config{$h}{'login'},
+ password => $config{$h}{'password'},
+ ) // '';
+ if ($reply eq '') {
+ failed("updating %s: Could not connect to %s.", $h, $config{$h}{'server'});
+ next;
+ }
+ next if !header_ok($h, $reply);
+
+ my @reply = split /\n/, $reply;
+ my ($title, $return_code, $error_code) = ('', '', '');
+ foreach my $line (@reply) {
+ $title = $1 if $line =~ m%\s*(.*)\s*%i;
+ $return_code = $1 if $line =~ m%^return\s+code\s*:\s*(.*)\s*$%i;
+ $error_code = $1 if $line =~ m%^error\s+code\s*:\s*(.*)\s*$%i;
+ }
+
+ if ($return_code ne 'NOERROR' || $error_code ne 'NOERROR' || !$title) {
+ $config{$h}{'status'} = 'failed';
+ $title = "incomplete response from $config{$h}{server}" unless $title;
+ warning("SENT: %s", $url) unless opt('verbose');
+ warning("REPLIED: %s", $reply);
+ failed("updating %s: %s", $h, $title);
+
+ } else {
+ $config{$h}{'ip'} = $ip;
+ $config{$h}{'mtime'} = $now;
+ $config{$h}{'status'} = 'good';
+ success("updating %s: %s: IP address set to %s (%s)", $h, $return_code, $ip, $title);
+ }
+ }
+}
+######################################################################
+## nic_dyndns2_updateable
+######################################################################
+sub nic_dyndns2_updateable {
+ my $host = shift;
+ my $update = 0;
+
+ if ($config{$host}{'mx'} ne $cache{$host}{'mx'}) {
+ info("forcing updating %s because 'mx' has changed to %s.", $host, $config{$host}{'mx'});
+ $update = 1;
+
+ } elsif ($config{$host}{'mx'} && (ynu($config{$host}{'backupmx'}, 1, 2, 3) ne ynu($config{$host}{'backupmx'}, 1, 2, 3))) {
+ info("forcing updating %s because 'backupmx' has changed to %s.", $host, ynu($config{$host}{'backupmx'}, "YES", "NO", "NO"));
+ $update = 1;
+
+ } elsif ($config{$host}{'static'} ne $cache{$host}{'static'}) {
+
+ info("forcing updating %s because 'static' has changed to %s.", $host, ynu($config{$host}{'static'}, "YES", "NO", "NO"));
+ $update = 1;
+
+ }
+ return $update;
+}
+######################################################################
+## nic_dyndns2_examples
+######################################################################
+sub nic_dyndns2_examples {
+ return <<"EoEXAMPLE";
+o 'dyndns2'
+
+The 'dyndns2' protocol is a newer low-bandwidth protocol used by a
+free dynamic DNS service offered by www.dyndns.org. It supports
+features of the older 'dyndns1' in addition to others. [These will be
+supported in a future version of ${program}.]
+
+Configuration variables applicable to the 'dyndns2' protocol are:
+ protocol=dyndns2 ##
+ server=fqdn.of.service ## defaults to members.dyndns.org
+ script=/path/to/script ## defaults to /nic/update
+ backupmx=no|yes ## indicates that this host is the primary MX for the domain.
+ static=no|yes ## indicates that this host has a static IP address.
+ custom=no|yes ## indicates that this host is a 'custom' top-level domain name.
+ mx=any.host.domain ## a host MX'ing for this host definition.
+ wildcard=no|yes ## add a DNS wildcard CNAME record that points to {host}
+ login=service-login ## login name and password registered with the service
+ password=service-password ##
+ fully.qualified.host ## the host registered with the service.
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=dyndns2, \\
+ login=my-dyndns.org-login, \\
+ password=my-dyndns.org-password \\
+ myhost.dyndns.org
+
+ ## multiple host update with wildcard'ing mx, and backupmx
+ protocol=dyndns2, \\
+ login=my-dyndns.org-login, \\
+ password=my-dyndns.org-password, \\
+ mx=a.host.willing.to.mx.for.me,backupmx=yes,wildcard=yes \\
+ myhost.dyndns.org,my2ndhost.dyndns.org
+
+ ## multiple host update to the custom DNS service
+ protocol=dyndns2, \\
+ login=my-dyndns.org-login, \\
+ password=my-dyndns.org-password \\
+ my-toplevel-domain.com,my-other-domain.com
+EoEXAMPLE
+}
+######################################################################
+## nic_dyndns2_update
+######################################################################
+sub nic_dyndns2_update {
+ debug("\nnic_dyndns2_update -------------------");
+
+ ## group hosts with identical attributes together
+ my %groups = group_hosts_by([ @_ ], [ qw(login password server static custom wildcard mx backupmx) ]);
+
+ my %errors = (
+ 'badauth' => 'Bad authorization (username or password)',
+ 'badsys' => 'The system parameter given was not valid',
+
+ 'notfqdn' => 'A Fully-Qualified Domain Name was not provided',
+ 'nohost' => 'The hostname specified does not exist in the database',
+ '!yours' => 'The hostname specified exists, but not under the username currently being used',
+ '!donator' => 'The offline setting was set, when the user is not a donator',
+ '!active' => 'The hostname specified is in a Custom DNS domain which has not yet been activated.',
+ 'abuse', => 'The hostname specified is blocked for abuse; you should receive an email notification ' .
+ 'which provides an unblock request link. More info can be found on ' .
+ 'https://www.dyndns.com/support/abuse.html',
+
+ 'numhost' => 'System error: Too many or too few hosts found. Contact support@dyndns.org',
+ 'dnserr' => 'System error: DNS error encountered. Contact support@dyndns.org',
+
+ 'nochg' => 'No update required; unnecessary attempts to change to the current address are considered abusive',
+ );
+
+ ## update each set of hosts that had similar configurations
+ foreach my $sig (keys %groups) {
+ my @hosts = @{$groups{$sig}};
+ my $hosts = join(',', @hosts);
+ my $h = $hosts[0];
+ my $ip = $config{$h}{'wantip'};
+ delete $config{$_}{'wantip'} foreach @hosts;
+
+ info("setting IP address to %s for %s", $ip, $hosts);
+ verbose("UPDATE:", "updating %s", $hosts);
+
+ ## Select the DynDNS system to update
+ my $url = "http://$config{$h}{'server'}$config{$h}{'script'}?system=";
+ if ($config{$h}{'custom'}) {
+ warning("updating %s: 'custom' and 'static' may not be used together. ('static' ignored)", $hosts)
+ if $config{$h}{'static'};
+ $url .= 'custom';
+
+ } elsif ($config{$h}{'static'}) {
+ $url .= 'statdns';
+
+ } else {
+ $url .= 'dyndns';
+ }
+
+ $url .= "&hostname=$hosts";
+ $url .= "&myip=";
+ $url .= $ip if $ip;
+
+ ## some args are not valid for a custom domain.
+ $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');
+ }
+
+ my $reply = geturl(
+ proxy => opt('proxy'),
+ url => $url,
+ login => $config{$h}{'login'},
+ password => $config{$h}{'password'},
+ ) // '';
+ if ($reply eq '') {
+ failed("updating %s: Could not connect to %s.", $hosts, $config{$h}{'server'});
+ next;
+ }
+ next if !header_ok($hosts, $reply);
+
+ my @reply = split /\n/, $reply;
+ my $state = 'header';
+ my $returnedip = $ip;
+
+ foreach my $line (@reply) {
+ if ($state eq 'header') {
+ $state = 'body';
+
+ } elsif ($state eq 'body') {
+ $state = 'results' if $line eq '';
+
+ } elsif ($state =~ /^results/) {
+ $state = 'results2';
+
+ # bug #10: some dyndns providers does not return the IP so
+ # we can't use the returned IP
+ my ($status, $returnedip) = split / /, lc $line;
+ $ip = $returnedip if (not $ip);
+ my $h = shift @hosts;
+
+ $config{$h}{'status'} = $status;
+ if ($status eq 'good') {
+ $config{$h}{'ip'} = $ip;
+ $config{$h}{'mtime'} = $now;
+ success("updating %s: %s: IP address set to %s", $h, $status, $ip);
+
+ } elsif (exists $errors{$status}) {
+ if ($status eq 'nochg') {
+ warning("updating %s: %s: %s", $h, $status, $errors{$status});
+ $config{$h}{'ip'} = $ip;
+ $config{$h}{'mtime'} = $now;
+ $config{$h}{'status'} = 'good';
+
+ } else {
+ failed("updating %s: %s: %s", $h, $status, $errors{$status});
+ }
+
+ } elsif ($status =~ /w(\d+)(.)/) {
+ my ($wait, $units) = ($1, lc $2);
+ my ($sec, $scale) = ($wait, 1);
+
+ ($scale, $units) = (1, 'seconds') if $units eq 's';
+ ($scale, $units) = (60, 'minutes') if $units eq 'm';
+ ($scale, $units) = (60*60, 'hours') if $units eq 'h';
+
+ $sec = $wait * $scale;
+ $config{$h}{'wtime'} = $now + $sec;
+ warning("updating %s: %s: wait %s %s before further updates", $h, $status, $wait, $units);
+
+ } else {
+ failed("updating %s: unexpected status (%s)", $h, $line);
+ }
+ }
+ }
+ failed("updating %s: Could not connect to %s.", $hosts, $config{$h}{'server'})
+ if $state ne 'results2';
+ }
+}
+
+######################################################################
+## nic_dnsexit_examples
+######################################################################
+sub nic_dnsexit_examples {
+ return <<"EoEXAMPLE";
+o 'dnsexit'
+
+The 'dnsexit' protocol is the protocol used by the dynamic hostname services
+of the 'DnsExit' dns services. This is currently used by the free
+dynamic DNS service offered by www.dnsexit.com.
+
+Configuration variables applicable to the 'dnsexit' protocol are:
+ ssl=no ## turn off ssl
+ protocol=dnsexit ##
+ server=update.dnsexit.com ## defaults to update.dnsexit.com
+ use=web ## defaults to web
+ web=update.dnsexit.com ## defaults to update.dnsexit.com
+ script=/RemoteUpdate.sv ## defaults to /RemoteUpdate.sv
+ login=service-userid ## userid registered with the service
+ password=service-password ## password registered with the service
+ fully.qualified.host ## the host registered with the service.
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=dnsexit \\
+ login=service-userid \\
+ password=service-password \\
+ fully.qualified.host
+
+EoEXAMPLE
+}
+######################################################################
+## nic_dnsexit_update
+##
+## written by Gonzalo Pérez de Olaguer Córdoba
+##
+## based on https://www.dnsexit.com/Direct.sv?cmd=ipClients
+## fetches this URL to update:
+## https://update.dnsexit.com/RemoteUpdate.sv?login=yourlogin&password=yourpassword&
+## host=yourhost.yourdomain.com&myip=xxx.xx.xx.xxx
+##
+######################################################################
+sub nic_dnsexit_update {
+ debug("\nnic_dnsexit_update -------------------");
+
+ my %status = (
+ '0' => [ 'good', 'Success' ],
+ '1' => [ 'nochg', 'IP is the same as the IP on the system' ],
+ '2' => [ 'badauth', 'Invalid password' ],
+ '3' => [ 'badauth', 'User not found' ],
+ '4' => [ 'nochg', 'IP not changed. To save our system resources, please don\'t post updates unless the IP got changed.' ],
+ '10' => [ 'error', 'Hostname is not specified' ],
+ '11' => [ 'nohost', 'fail to find the domain' ],
+ '13' => [ 'error', 'parameter validation error' ],
+ );
+
+ ## update each configured host
+ foreach my $h (@_) {
+ my $ip = delete $config{$h}{'wantip'};
+ info("setting IP address to %s for %s", $ip, $h);
+ verbose("UPDATE:","updating %s", $h);
+
+ # Set the URL that we're going to update
+ my $url;
+ $url = "https://$config{$h}{'server'}$config{$h}{'script'}";
+ $url .= "?login=$config{$h}{'login'}";
+ $url .= "&password=$config{$h}{'password'}";
+ $url .= "&host=$h";
+ $url .= "&myip=";
+ $url .= $ip if $ip;
+
+ # Try to get URL
+ my $reply = geturl(
+ proxy => opt('proxy'),
+ url => $url
+ );
+
+ # No response, declare as failed
+ if (!defined($reply) || !$reply) {
+ failed("updating %s: Could not connect to %s.", $h, $config{$h}{'server'});
+ last;
+ }
+ last if !header_ok($h, $reply);
+
+ # Response found
+ if ($reply =~ /(\d+)=(.+)/) {
+ my ($statuscode, $statusmsg) = ($1, $2);
+ if (exists $status{$statuscode}) {
+ my ($status, $message) = @{ $status{$statuscode} };
+ if ($status =~ m'^(good|nochg)$') {
+ $config{$h}{'ip'} = $ip;
+ $config{$h}{'mtime'} = $now;
+ }
+ $config{$h}{'status'} = $status;
+ if ($status eq 'good') {
+ success("updating %s: good: IP address set to %s", $h, $ip);
+ } else {
+ warning("updating %s: %s: %s", $h, $status, $message);
+ }
+ } else {
+ $config{$h}{'status'} = 'failed';
+ failed("updating %s: failed: unrecognized status code (%s)", $h, $statuscode);
+ }
+ } else {
+ $config{$h}{'status'} = 'failed';
+ warning("SENT: %s", $url) unless opt('verbose');
+ warning("REPLIED: %s", $reply);
+ failed("updating %s: unrecognized reply.", $h);
+ }
+ }
+}
+######################################################################
+## nic_noip_update
+## Note: uses same features as nic_dyndns2_update, less return codes
+######################################################################
+sub nic_noip_update {
+ debug("\nnic_noip_update -------------------");
+
+ ## group hosts with identical attributes together
+ my %groups = group_hosts_by([ @_ ], [ qw(login password server static custom wildcard mx backupmx) ]);
+
+ my %errors = (
+ 'badauth' => 'Invalid username or password',
+ 'badagent' => 'Invalid user agent',
+ 'nohost' => 'The hostname specified does not exist in the database',
+ '!donator' => 'The offline setting was set, when the user is not a donator',
+ 'abuse', => 'The hostname specified is blocked for abuse; open a trouble ticket at http://www.no-ip.com',
+ 'numhost' => 'System error: Too many or too few hosts found. open a trouble ticket at http://www.no-ip.com',
+ 'dnserr' => 'System error: DNS error encountered. Contact support@dyndns.org',
+ 'nochg' => 'No update required; unnecessary attempts to change to the current address are considered abusive',
+ );
+
+ ## update each set of hosts that had similar configurations
+ foreach my $sig (keys %groups) {
+ my @hosts = @{$groups{$sig}};
+ my $hosts = join(',', @hosts);
+ my $h = $hosts[0];
+ my $ip = $config{$h}{'wantip'};
+ delete $config{$_}{'wantip'} foreach @hosts;
+
+ info("setting IP address to %s for %s", $ip, $hosts);
+ verbose("UPDATE:", "updating %s", $hosts);
+
+ my $url = "https://$config{$h}{'server'}/nic/update?system=";
+ $url .= 'noip';
+ $url .= "&hostname=$hosts";
+ $url .= "&myip=";
+ $url .= $ip if $ip;
+
+ my $reply = geturl(
+ proxy => opt('proxy'),
+ url => $url,
+ login => $config{$h}{'login'},
+ password => $config{$h}{'password'},
+ ) // '';
+ if ($reply eq '') {
+ failed("updating %s: Could not connect to %s.", $hosts, $config{$h}{'server'});
+ next;
+ }
+ next if !header_ok($hosts, $reply);
+
+ my @reply = split /\n/, $reply;
+ my $state = 'header';
+ foreach my $line (@reply) {
+ if ($state eq 'header') {
+ $state = 'body';
+
+ } elsif ($state eq 'body') {
+ $state = 'results' if $line eq '';
+
+ } elsif ($state =~ /^results/) {
+ $state = 'results2';
+
+ my ($status, $ip) = split / /, lc $line;
+ my $h = shift @hosts;
+
+ $config{$h}{'status'} = $status;
+ if ($status eq 'good') {
+ $config{$h}{'ip'} = $ip;
+ $config{$h}{'mtime'} = $now;
+ success("updating %s: %s: IP address set to %s", $h, $status, $ip);
+
+ } elsif (exists $errors{$status}) {
+ if ($status eq 'nochg') {
+ warning("updating %s: %s: %s", $h, $status, $errors{$status});
+ $config{$h}{'ip'} = $ip;
+ $config{$h}{'mtime'} = $now;
+ $config{$h}{'status'} = 'good';
+
+ } else {
+ failed("updating %s: %s: %s", $h, $status, $errors{$status});
+ }
+
+ } elsif ($status =~ /w(\d+)(.)/) {
+ my ($wait, $units) = ($1, lc $2);
+ my ($sec, $scale) = ($wait, 1);
+
+ ($scale, $units) = (1, 'seconds') if $units eq 's';
+ ($scale, $units) = (60, 'minutes') if $units eq 'm';
+ ($scale, $units) = (60*60, 'hours') if $units eq 'h';
+
+ $sec = $wait * $scale;
+ $config{$h}{'wtime'} = $now + $sec;
+ warning("updating %s: %s: wait %s %s before further updates", $h, $status, $wait, $units);
+
+ } else {
+ failed("updating %s: unexpected status (%s)", $h, $line);
+ }
+ }
+ }
+ failed("updating %s: Could not connect to %s.", $hosts, $config{$h}{'server'})
+ if $state ne 'results2';
+ }
+}
+######################################################################
+## nic_noip_examples
+######################################################################
+sub nic_noip_examples {
+ return <<"EoEXAMPLE";
+o 'noip'
+
+The 'No-IP Compatible' protocol is used to make dynamic dns updates
+over an http request. Details of the protocol are outlined at:
+https://www.noip.com/integrate/
+
+Configuration variables applicable to the 'noip' protocol are:
+ protocol=noip ##
+ server=fqdn.of.service ## defaults to dynupdate.no-ip.com
+ login=service-login ## login name and password registered with the service
+ password=service-password ##
+ fully.qualified.host ## the host registered with the service.
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=noip, \\
+ login=userlogin\@domain.com, \\
+ password=noip-password \\
+ myhost.no-ip.biz
+
+
+EoEXAMPLE
+}
+######################################################################
+## nic_dslreports1_examples
+######################################################################
+sub nic_dslreports1_examples {
+ return <<"EoEXAMPLE";
+o 'dslreports1'
+
+The 'dslreports1' protocol is used by a free DSL monitoring service
+offered by www.dslreports.com.
+
+Configuration variables applicable to the 'dslreports1' protocol are:
+ protocol=dslreports1 ##
+ server=fqdn.of.service ## defaults to www.dslreports.com
+ login=service-login ## login name and password registered with the service
+ password=service-password ##
+ unique-number ## the host registered with the service.
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=dslreports1, \\
+ login=my-dslreports-login, \\
+ password=my-dslreports-password \\
+ 123456
+
+Note: DSL Reports uses a unique number as the host name. This number
+can be found on the Monitor Control web page.
+EoEXAMPLE
+}
+######################################################################
+## nic_dslreports1_update
+######################################################################
+sub nic_dslreports1_update {
+ debug("\nnic_dslreports1_update -------------------");
+ ## update each configured host
+ foreach my $h (@_) {
+ my $ip = delete $config{$h}{'wantip'};
+ info("setting IP address to %s for %s", $ip, $h);
+ verbose("UPDATE:", "updating %s", $h);
+
+ my $url;
+ $url = "https://$config{$h}{'server'}/nic/";
+ $url .= ynu($config{$h}{'static'}, 'statdns', 'dyndns', 'dyndns');
+ $url .= "?action=edit&started=1&hostname=YES&host_id=$h";
+ $url .= "&myip=";
+ $url .= $ip if $ip;
+
+ my $reply = geturl(
+ proxy => opt('proxy'),
+ url => $url,
+ login => $config{$h}{'login'},
+ password => $config{$h}{'password'},
+ ) // '';
+ if ($reply eq '') {
+ failed("updating %s: Could not connect to %s.", $h, $config{$h}{'server'});
+ next;
+ }
+
+ my @reply = split /\n/, $reply;
+ my $return_code = '';
+ foreach my $line (@reply) {
+ $return_code = $1 if $line =~ m%^return\s+code\s*:\s*(.*)\s*$%i;
+ }
+
+ if ($return_code !~ /NOERROR/) {
+ $config{$h}{'status'} = 'failed';
+ warning("SENT: %s", $url) unless opt('verbose');
+ warning("REPLIED: %s", $reply);
+ failed("updating %s", $h);
+
+ } else {
+ $config{$h}{'ip'} = $ip;
+ $config{$h}{'mtime'} = $now;
+ $config{$h}{'status'} = 'good';
+ success("updating %s: %s: IP address set to %s", $h, $return_code, $ip);
+ }
+ }
+}
+
+######################################################################
+## nic_zoneedit1_examples
+######################################################################
+sub nic_zoneedit1_examples {
+ return <<"EoEXAMPLE";
+o 'zoneedit1'
+
+The 'zoneedit1' protocol is used by a DNS service offered by
+www.zoneedit.com.
+
+Configuration variables applicable to the 'zoneedit1' protocol are:
+ protocol=zoneedit1 ##
+ server=fqdn.of.service ## defaults to www.zoneedit.com
+ zone=zone-where-domains-are ## only needed if 1 or more subdomains are deeper
+ ## than 1 level in relation to the zone where it
+ ## is defined. For example, b.foo.com in a zone
+ ## foo.com doesn't need this, but a.b.foo.com in
+ ## the same zone needs zone=foo.com
+ login=service-login ## login name and password registered with the service
+ password=service-password ##
+ your.domain.name ## the host registered with the service.
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=zoneedit1, \\
+ server=dynamic.zoneedit.com, \\
+ zone=zone-where-domains-are, \\
+ login=my-zoneedit-login, \\
+ password=my-zoneedit-password \\
+ my.domain.name
+EoEXAMPLE
+}
+
+######################################################################
+## nic_zoneedit1_updateable
+######################################################################
+sub nic_zoneedit1_updateable {
+ return 0;
+}
+
+######################################################################
+## nic_zoneedit1_update
+#
+#
+#
+######################################################################
+sub nic_zoneedit1_update {
+ debug("\nnic_zoneedit1_update -------------------");
+
+ ## group hosts with identical attributes together
+ my %groups = group_hosts_by([ @_ ], [ qw(login password server zone) ]);
+
+ ## update each set of hosts that had similar configurations
+ foreach my $sig (keys %groups) {
+ my @hosts = @{$groups{$sig}};
+ my $hosts = join(',', @hosts);
+ my $h = $hosts[0];
+ my $ip = $config{$h}{'wantip'};
+ delete $config{$_}{'wantip'} foreach @hosts;
+
+ info("setting IP address to %s for %s", $ip, $hosts);
+ verbose("UPDATE:", "updating %s", $hosts);
+
+ my $url = '';
+ $url .= "https://$config{$h}{'server'}/auth/dynamic.html";
+ $url .= "?host=$hosts";
+ $url .= "&dnsto=$ip" if $ip;
+ $url .= "&zone=$config{$h}{'zone'}" if defined $config{$h}{'zone'};
+
+ my $reply = geturl(
+ proxy => opt('proxy'),
+ url => $url,
+ login => $config{$h}{'login'},
+ password => $config{$h}{'password'},
+ ) // '';
+ if ($reply eq '') {
+ failed("updating %s: Could not connect to %s.", $hosts, $config{$h}{'server'});
+ next;
+ }
+ next if !header_ok($hosts, $reply);
+
+ my @reply = split /\n/, $reply;
+ foreach my $line (@reply) {
+ if ($line =~ /^[^<]*<(SUCCESS|ERROR)\s+([^>]+)>(.*)/) {
+ my ($status, $assignments, $rest) = ($1, $2, $3);
+ my ($left, %var) = parse_assignments($assignments);
+
+ if (keys %var) {
+ my ($status_code, $status_text, $status_ip) = ('999', '', $ip);
+ $status_code = $var{'CODE'} if exists $var{'CODE'};
+ $status_text = $var{'TEXT'} if exists $var{'TEXT'};
+ $status_ip = $var{'IP'} if exists $var{'IP'};
+
+ if ($status eq 'SUCCESS' || ($status eq 'ERROR' && $var{'CODE'} eq '707')) {
+ $config{$h}{'ip'} = $status_ip;
+ $config{$h}{'mtime'} = $now;
+ $config{$h}{'status'} = 'good';
+
+ success("updating %s: IP address set to %s (%s: %s)", $h, $ip, $status_code, $status_text);
+
+ } else {
+ $config{$h}{'status'} = 'failed';
+ failed("updating %s: %s: %s", $h, $status_code, $status_text);
+ }
+ shift @hosts;
+ $h = $hosts[0];
+ $hosts = join(',', @hosts);
+ }
+ $line = $rest;
+ redo if $line;
+ }
+ }
+ failed("updating %s: no response from %s", $hosts, $config{$h}{'server'})
+ if @hosts;
+ }
+}
+######################################################################
+## nic_easydns_updateable
+######################################################################
+sub nic_easydns_updateable {
+ my $host = shift;
+ my $update = 0;
+
+ if ($config{$host}{'mx'} ne $cache{$host}{'mx'}) {
+ info("forcing updating %s because 'mx' has changed to %s.", $host, $config{$host}{'mx'});
+ $update = 1;
+
+ } elsif ($config{$host}{'mx'} && (ynu($config{$host}{'backupmx'}, 1, 2, 3) ne ynu($config{$host}{'backupmx'}, 1, 2, 3))) {
+ info("forcing updating %s because 'backupmx' has changed to %s.", $host, ynu($config{$host}{'backupmx'}, "YES", "NO", "NO"));
+ $update = 1;
+
+ } elsif ($config{$host}{'static'} ne $cache{$host}{'static'}) {
+
+ info("forcing updating %s because 'static' has changed to %s.", $host, ynu($config{$host}{'static'}, "YES", "NO", "NO"));
+ $update = 1;
+
+ }
+ return $update;
+}
+######################################################################
+## nic_easydns_examples
+######################################################################
+sub nic_easydns_examples {
+ return <<"EoEXAMPLE";
+o 'easydns'
+
+The 'easydns' protocol is used by the for fee DNS service offered
+by www.easydns.com.
+
+Configuration variables applicable to the 'easydns' protocol are:
+ protocol=easydns ##
+ server=fqdn.of.service ## defaults to members.easydns.com
+ backupmx=no|yes ## indicates that EasyDNS should be the secondary MX
+ ## for this domain or host.
+ mx=any.host.domain ## a host MX'ing for this host or domain.
+ wildcard=no|yes ## add a DNS wildcard CNAME record that points to {host}
+ login=service-login ## login name and password registered with the service
+ password=service-password ##
+ fully.qualified.host ## the host registered with the service.
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=easydns, \\
+ login=my-easydns.com-login, \\
+ password=my-easydns.com-password \\
+ myhost.easydns.com
+
+ ## multiple host update with wildcard'ing mx, and backupmx
+ protocol=easydns, \\
+ login=my-easydns.com-login, \\
+ password=my-easydns.com-password, \\
+ mx=a.host.willing.to.mx.for.me, \\
+ backupmx=yes, \\
+ wildcard=yes \\
+ my-toplevel-domain.com,my-other-domain.com
+
+ ## multiple host update to the custom DNS service
+ protocol=easydns, \\
+ login=my-easydns.com-login, \\
+ password=my-easydns.com-password \\
+ my-toplevel-domain.com,my-other-domain.com
+EoEXAMPLE
+}
+######################################################################
+## nic_easydns_update
+######################################################################
+sub nic_easydns_update {
+ debug("\nnic_easydns_update -------------------");
+
+ ## each host is in a group by itself
+ my %groups = map { $_ => [ $_ ] } @_;
+
+ my %errors = (
+ 'NOACCESS' => 'Authentication failed. This happens if the username/password OR host or domain are wrong.',
+ 'NOSERVICE' => 'Dynamic DNS is not turned on for this domain.',
+ 'ILLEGAL' => 'Client sent data that is not allowed in a dynamic DNS update.',
+ 'TOOSOON' => 'Update frequency is too short.',
+ );
+
+ ## update each set of hosts that had similar configurations
+ foreach my $sig (keys %groups) {
+ my @hosts = @{$groups{$sig}};
+ my $hosts = join(',', @hosts);
+ my $h = $hosts[0];
+ my $ip = $config{$h}{'wantip'};
+ delete $config{$_}{'wantip'} foreach @hosts;
+
+ info("setting IP address to %s for %s", $ip, $hosts);
+ verbose("UPDATE:", "updating %s", $hosts);
+
+ #'https://api.cp.easydns.com/dyn/generic.php?hostname=test.burry.ca&myip=10.20.30.40&wildcard=ON'
+
+ my $url;
+ $url = "https://$config{$h}{'server'}$config{$h}{'script'}?";
+ $url .= "hostname=$hosts";
+ $url .= "&myip=";
+ $url .= $ip if $ip;
+ $url .= "&wildcard=" . ynu($config{$h}{'wildcard'}, 'ON', 'OFF', 'OFF') if defined $config{$h}{'wildcard'};
+
+ if ($config{$h}{'mx'}) {
+ $url .= "&mx=$config{$h}{'mx'}";
+ $url .= "&backmx=" . ynu($config{$h}{'backupmx'}, 'YES', 'NO');
+ }
+
+ my $reply = geturl(
+ proxy => opt('proxy'),
+ url => $url,
+ login => $config{$h}{'login'},
+ password => $config{$h}{'password'},
+ ) // '';
+ if ($reply eq '') {
+ failed("updating %s: Could not connect to %s.", $hosts, $config{$h}{'server'});
+ next;
+ }
+ next if !header_ok($hosts, $reply);
+
+ my @reply = split /\n/, $reply;
+ my $state = 'header';
+ foreach my $line (@reply) {
+ if ($state eq 'header') {
+ $state = 'body';
+
+ } elsif ($state eq 'body') {
+ $state = 'results' if $line eq '';
+
+ } elsif ($state =~ /^results/) {
+ $state = 'results2';
+
+ my ($status) = $line =~ /^(\S*)\b.*/;
+ my $h = shift @hosts;
+
+ $config{$h}{'status'} = $status;
+ if ($status eq 'NOERROR') {
+ $config{$h}{'ip'} = $ip;
+ $config{$h}{'mtime'} = $now;
+ success("updating %s: %s: IP address set to %s", $h, $status, $ip);
+
+ } elsif ($status =~ /TOOSOON/) {
+ ## make sure we wait at least a little
+ my ($wait, $units) = (5, 'm');
+ my ($sec, $scale) = ($wait, 1);
+
+ ($scale, $units) = (1, 'seconds') if $units eq 's';
+ ($scale, $units) = (60, 'minutes') if $units eq 'm';
+ ($scale, $units) = (60*60, 'hours') if $units eq 'h';
+ $config{$h}{'wtime'} = $now + $sec;
+ warning("updating %s: %s: wait %d %s before further updates", $h, $status, $wait, $units);
+
+ } elsif (exists $errors{$status}) {
+ failed("updating %s: %s: %s", $h, $line, $errors{$status});
+
+ } else {
+ failed("updating %s: unexpected status (%s)", $h, $line);
+ }
+ last;
+ }
+ }
+ failed("updating %s: Could not connect to %s.", $hosts, $config{$h}{'server'})
+ if $state ne 'results2';
+ }
+}
+######################################################################
+
+######################################################################
+## nic_namecheap_examples
+######################################################################
+sub nic_namecheap_examples {
+ return <<"EoEXAMPLE";
+
+o 'namecheap'
+
+The 'namecheap' protocol is used by DNS service offered by www.namecheap.com.
+
+Configuration variables applicable to the 'namecheap' protocol are:
+ protocol=namecheap ##
+ server=fqdn.of.service ## defaults to dynamicdns.park-your-domain.com
+ login=service-login ## the domain of the dynamic DNS record you want to update
+ password=service-password ## Generated password for your dynamic DNS record
+ hostname ## the subdomain to update, use @ for base domain name, * for catch all
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=namecheap \\
+ login=example.com \\
+ password=example.com-generated-password \\
+ @
+
+EoEXAMPLE
+}
+######################################################################
+## nic_namecheap_update
+##
+## written by Dan Boardman
+##
+## based on https://www.namecheap.com/support/knowledgebase/
+## article.aspx/29/11/how-to-use-the-browser-to-dynamically-update-hosts-ip
+## needs this url to update:
+## https://dynamicdns.park-your-domain.com/update?host=host_name&
+## domain=domain.com&password=domain_password[&ip=your_ip]
+##
+######################################################################
+sub nic_namecheap_update {
+
+
+ debug("\nnic_namecheap1_update -------------------");
+
+ ## update each configured host
+ foreach my $h (@_) {
+ my $ip = delete $config{$h}{'wantip'};
+ info("setting IP address to %s for %s", $ip, $h);
+ verbose("UPDATE:", "updating %s", $h);
+
+ my $url;
+ $url = "https://$config{$h}{'server'}/update";
+ my $domain = $config{$h}{'login'};
+ my $host = $h;
+ $host =~ s/(.*)\.$domain(.*)/$1$2/;
+ $url .= "?host=$host";
+ $url .= "&domain=$domain";
+ $url .= "&password=$config{$h}{'password'}";
+ $url .= "&ip=";
+ $url .= $ip if $ip;
+
+ my $reply = geturl(proxy => opt('proxy'), url => $url) // '';
+ if ($reply eq '') {
+ failed("updating %s: Could not connect to %s.", $h, $config{$h}{'server'});
+ next;
+ }
+ next if !header_ok($h, $reply);
+
+ my @reply = split /\n/, $reply;
+ if (grep /0/i, @reply) {
+ $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';
+ warning("SENT: %s", $url) unless opt('verbose');
+ warning("REPLIED: %s", $reply);
+ failed("updating %s: Invalid reply.", $h);
+ }
+ }
+}
+
+######################################################################
+
+######################################################################
+## 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. Use this URL to get your API-Key-password:
+ https://members.nearlyfreespeech.net/support/assist?tag=apikey
+
+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;
+ $auth_header .= "\n";
+
+ 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 .= "Content-Type: application/x-www-form-urlencoded\n";
+ }
+
+ return geturl(
+ proxy => opt('proxy'),
+ url => $url,
+ headers => $header,
+ method => $method,
+ data => $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("%s", $json->{'error'});
+ if (defined $json->{'debug'}) {
+ failed("%s", $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 = $h;
+ $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);
+ }
+ }
+}
+
+######################################################################
+
+######################################################################
+## nic_njalla_examples
+######################################################################
+sub nic_njalla_examples {
+ return <<"EoEXAMPLE";
+
+o 'njalla'
+
+The 'njalla' protocol is used by DNS service offered by njal.la.
+
+Configuration variables applicable to the 'njalla' protocol are:
+ protocol=njalla ##
+ password=service-password ## Generated password for your dynamic DNS record
+ quietreply=no|yes ## If yes return empty response on success with status 200 but print errors
+ domain ## subdomain to update, use @ for base domain name, * for catch all
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=njalla \\
+ password=njal.la-key
+ quietreply=no
+ domain.com
+
+EoEXAMPLE
+}
+######################################################################
+## nic_njalla_update
+##
+## written by satrapes
+##
+## based on https://njal.la/docs/ddns/
+## needs this url to update:
+## https://njal.la/update?h=host_name&k=domain_password&a=your_ip
+## response contains "code 200" on succesful completion
+######################################################################
+sub nic_njalla_update {
+ debug("\nnic_njalla_update -------------------");
+
+ foreach my $h (@_) {
+ # Read input params
+ my $ipv4 = delete $config{$h}{'wantipv4'};
+ my $ipv6 = delete $config{$h}{'wantipv6'};
+ my $quietreply = delete $config{$h}{'quietreply'};
+ my $ip_output = '';
+
+ # Build url
+ my $url = "https://$config{$h}{'server'}/update/?h=$h&k=$config{$h}{'password'}";
+ my $auto = 1;
+ foreach my $ip ($ipv4, $ipv6) {
+ next if (!$ip);
+ $auto = 0;
+ my $ipv = ($ip eq ($ipv6 // '')) ? '6' : '4';
+ my $type = ($ip eq ($ipv6 // '')) ? 'aaaa' : 'a';
+ $ip_output .= " IP v$ipv: $ip,";
+ $url .= "&$type=$ip";
+ }
+ $url .= (($auto eq 1)) ? '&auto' : '';
+ $url .= (($quietreply eq 1)) ? '&quiet' : '';
+
+ info("setting address to%s for %s", ($ip_output eq '') ? ' auto' : $ip_output, $h);
+ verbose("UPDATE:", "updating %s", $h);
+ debug("url: %s", $url);
+
+ # Try to get URL
+ my $reply = geturl(proxy => opt('proxy'), url => $url);
+ my $response = '';
+ if ($quietreply) {
+ $reply =~ qr/invalid host or key/mp;
+ $response = ${^MATCH};
+ if (!$response) {
+ success("updating %s: good: IP address set to %s", $h, $ip_output);
+ }
+ elsif ($response =~ /invalid host or key/) {
+ failed("Invalid host or key");
+ } else {
+ failed("Unknown response");
+ }
+ } else {
+ $reply =~ qr/{(?:[^{}]*|(?R))*}/mp;
+ $response = eval {decode_json(${^MATCH})};
+ # No response, declare as failed
+ if (!defined($reply) || !$reply) {
+ failed("updating %s: Could not connect to %s.", $h, $config{$h}{'server'});
+ } else {
+ # Strip header
+ if ($response->{status} == 401 && $response->{message} =~ /invalid host or key/) {
+ failed("Invalid host or key");
+ } elsif ($response->{status} == 200 && $response->{message} =~ /record updated/) {
+ success("updating %s: good: IP address set to %s", $h, $response->{value}->{A});
+ } else {
+ failed("Unknown response");
+ }
+ }
+ }
+ }
+}
+
+######################################################################
+## nic_sitelutions_examples
+######################################################################
+sub nic_sitelutions_examples {
+ return <<"EoEXAMPLE";
+
+o 'sitelutions'
+
+The 'sitelutions' protocol is used by DNS services offered by www.sitelutions.com.
+
+Configuration variables applicable to the 'sitelutions' protocol are:
+ protocol=sitelutions ##
+ server=fqdn.of.service ## defaults to sitelutions.com
+ login=service-login ## login name and password registered with the service
+ password=service-password ##
+ A_record_id ## Id of the A record for the host registered with the service.
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=sitelutions, \\
+ login=my-sitelutions.com-login, \\
+ password=my-sitelutions.com-password \\
+ my-sitelutions.com-id_of_A_record
+
+EoEXAMPLE
+}
+######################################################################
+## nic_sitelutions_update
+##
+## written by Mike W. Smith
+##
+## based on https://www.sitelutions.com/help/dynamic_dns_clients#updatespec
+## needs this url to update:
+## https://www.sitelutions.com/dnsup?id=990331&user=myemail@mydomain.com&pass=SecretPass&ip=192.168.10.4
+## domain=domain.com&password=domain_password&ip=your_ip
+##
+######################################################################
+sub nic_sitelutions_update {
+
+
+ debug("\nnic_sitelutions_update -------------------");
+
+ ## update each configured host
+ foreach my $h (@_) {
+ my $ip = delete $config{$h}{'wantip'};
+ info("setting IP address to %s for %s", $ip, $h);
+ verbose("UPDATE:", "updating %s", $h);
+
+ my $url;
+ $url = "https://$config{$h}{'server'}/dnsup";
+ $url .= "?id=$h";
+ $url .= "&user=$config{$h}{'login'}";
+ $url .= "&pass=$config{$h}{'password'}";
+ $url .= "&ip=";
+ $url .= $ip if $ip;
+
+ my $reply = geturl(proxy => opt('proxy'), url => $url);
+ if (!defined($reply) || !$reply) {
+ failed("updating %s: Could not connect to %s.", $h, $config{$h}{'server'});
+ next;
+ }
+ next if !header_ok($h, $reply);
+
+ my @reply = split /\n/, $reply;
+ if (grep /success/i, @reply) {
+ $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';
+ warning("SENT: %s", $url) unless opt('verbose');
+ warning("REPLIED: %s", $reply);
+ failed("updating %s: Invalid reply.", $h);
+ }
+ }
+}
+
+######################################################################
+
+######################################################################
+## nic_freedns_examples
+######################################################################
+sub nic_freedns_examples {
+ return <<"EoEXAMPLE";
+
+o 'freedns'
+
+The 'freedns' protocol is used by DNS services offered by freedns.afraid.org.
+
+Configuration variables applicable to the 'freedns' protocol are:
+ protocol=freedns ##
+ server=fqdn.of.service ## defaults to freedns.afraid.org
+ login=service-login ## login name and password registered with the service
+ password=service-password ##
+ fully.qualified.host ## the host registered with the service.
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=freedns, \\
+ login=my-freedns.afraid.org-login, \\
+ password=my-freedns.afraid.org-password \\
+ myhost.afraid.com
+
+EoEXAMPLE
+}
+######################################################################
+## nic_freedns_update
+##
+## API v1 documented at https://freedns.afraid.org/api/
+##
+## An update requires two steps. The first is to get a list of records from:
+## https://freedns.afraid.org/api/?action=getdyndns&v=2&sha=
+## The returned list looks like:
+##
+## hostname1.example.com|1.2.3.4|http://example/update/url1
+## hostname1.example.com|dead::beef|http://example/update/url2
+## hostname2.example.com|5.6.7.8|http://example/update/url3
+## hostname2.example.com|9.10.11.12|http://example/update/url4
+## hostname3.example.com|cafe::f00d|http://example/update/url5
+## hostname4.example.com|NULL|http://example/update/url6
+##
+## The record's columns are separated by '|'. The first is the hostname, the second is the current
+## address, and the third is the record-specific update URL. There can be multiple records for the
+## same host, and they can even have the same address type. To update an IP address the record
+## must already exist of the type we want to update... We will not change a record type from
+## an IPv4 to IPv6 or viz versa. Records may exist with a NULL address which we will allow to be
+## updated with an IPv4 address, not an IPv6.
+##
+## The second step is to visit the appropriate record's update URL with
+## ?address= appended. "Updated" in the result means success, "fail" means
+## failure.
+######################################################################
+sub nic_freedns_update {
+ debug("\nnic_freedns_update -------------------");
+ # Separate the records that are currently holding IPv4 addresses from the records that are
+ # currently holding IPv6 addresses so that we can avoid switching a record to a different
+ # 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 = $url_tmpl) =~ s//$creds/;
+
+ my $reply = geturl(proxy => opt('proxy'),
+ url => $url
+ );
+ my $record_list_error = '';
+ if ($reply && header_ok($_[0], $reply)) {
+ $reply =~ s/^.*?\n\n//s; # Strip the headers.
+ for (split("\n", $reply)) {
+ my @rec = split(/\|/);
+ next if ($#rec < 2);
+ my $recs = is_ipv6($rec[1]) ? \%recs_ipv6 : \%recs_ipv4;
+ $recs->{$rec[0]} = \@rec;
+ # Update URL contains credentials that don't require login to use, so best to hide.
+ debug("host: %s, current address: %s, update URL: ", $rec[0], $rec[1]);
+ }
+ if (keys(%recs_ipv4) + keys(%recs_ipv6) == 0) {
+ chomp($reply);
+ $record_list_error = "failed to get record list from $url_tmpl: $reply";
+ }
+ } else {
+ $record_list_error = "failed to get record list from $url_tmpl";
+ }
+
+ foreach my $h (@_) {
+ next if (!$h);
+ my $ipv4 = delete $config{$h}{'wantipv4'};
+ my $ipv6 = delete $config{$h}{'wantipv6'};
+
+ if ($record_list_error ne '') {
+ $config{$h}{'status-ipv4'} = 'failed' if ($ipv4);
+ $config{$h}{'status-ipv6'} = 'failed' if ($ipv6);
+ failed("updating %s: %s", $h, $record_list_error);
+ next;
+ }
+
+ # IPv4 and IPv6 handling are similar enough to do in a loop...
+ foreach my $ip ($ipv4, $ipv6) {
+ next if (!$ip);
+ my $ipv = ($ip eq ($ipv6 // '')) ? '6' : '4';
+ my $type = ($ip eq ($ipv6 // '')) ? 'AAAA' : 'A';
+ my $rec = ($ip eq ($ipv6 // '')) ? $recs_ipv6{$h}
+ : $recs_ipv4{$h};
+ if (!$rec) {
+ failed("updating %s: Cannot set IPv$ipv to %s No '$type' record at FreeDNS", $h, $ip);
+ next;
+ }
+
+ info("updating %s: setting IP address to %s", $h, $ip);
+ $config{$h}{"status-ipv$ipv"} = 'failed';
+
+ if ($ip eq $rec->[1]) {
+ $config{$h}{"ipv$ipv"} = $ip;
+ $config{$h}{'mtime'} = $now;
+ $config{$h}{"status-ipv$ipv"} = 'good';
+ success("updating %s: update not necessary, '$type' record already set to %s", $h, $ip)
+ if (!$daemon || opt('verbose'));
+ } else {
+ my $url = $rec->[2] . "&address=" . $ip;
+ ($url_tmpl = $url) =~ s/\?.*\&/?&/; # redact unique update token
+ debug("updating: %s", $url_tmpl);
+
+ my $reply = geturl(proxy => opt('proxy'),
+ url => $url
+ );
+ if ($reply && header_ok($h, $reply)) {
+ $reply =~ s/^.*?\n\n//s; # Strip the headers.
+ if ($reply =~ /Updated.*$h.*to.*$ip/) {
+ $config{$h}{"ipv$ipv"} = $ip;
+ $config{$h}{'mtime'} = $now;
+ $config{$h}{"status-ipv$ipv"} = 'good';
+ success("updating %s: good: IPv$ipv address set to %s", $h, $ip);
+ } else {
+ warning("SENT: %s", $url_tmpl) unless opt('verbose');
+ warning("REPLIED: %s", $reply);
+ failed("updating %s: Invalid reply.", $h);
+ }
+ } else {
+ failed("updating %s: Could not connect to %s.", $h, $url_tmpl);
+ }
+ }
+ }
+ }
+}
+
+######################################################################
+## nic_1984_examples
+######################################################################
+sub nic_1984_examples {
+ return <<"EoEXAMPLE";
+
+o '1984'
+
+The '1984' protocol is used by DNS services offered by 1984.is.
+
+Configuration variables applicable to the '1984' protocol are:
+ protocol=1984 ##
+ password=api-key ## your API key
+ fully.qualified.host ## the domain to update
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=1984, \\
+ password=my-1984-api-key, \\
+ myhost
+
+EoEXAMPLE
+}
+
+######################################################################
+## nic_1984_update
+## https://api.1984.is/1.0/freedns/?apikey=xxx&domain=mydomain&ip=myip
+## The response is a JSON document containing the following entries
+## - ok: true or false depending on if the request was successful or not,
+## if the ip is the same as before this will be true,
+## - msg: successes or why it is not working,
+## - lookup: if domain or subdomain was not found lookup will contain a list of names tried
+######################################################################
+sub nic_1984_update {
+ debug("\nnic_1984_update -------------------");
+ foreach my $host (@_) {
+ my $ip = delete $config{$host}{'wantip'};
+ info("setting IP address to %s for %s", $ip, $host);
+ verbose("UPDATE:", "updating %s", $host);
+
+ my $url;
+ $url = "https://$config{$host}{'server'}/1.0/freedns/";
+ $url .= "?apikey=$config{$host}{'password'}";
+ $url .= "&domain=$host";
+ $url .= "&ip=$ip";
+
+ my $reply = geturl(
+ proxy => opt('proxy'),
+ url => $url,
+ ) // '';
+ if ($reply eq '') {
+ failed("Updating %s: Could not connect to %s.", $host, $config{$host}{'server'});
+ next;
+ }
+ next if !header_ok($host, $reply);
+
+ # Strip header
+ $reply =~ qr/{(?:[^{}]*|(?R))*}/mp;
+ my $response = eval { decode_json(${^MATCH}) };
+ if ($@) {
+ failed("Updating %s: JSON decoding failure", $host);
+ next;
+ }
+ unless ($response->{ok}) {
+ failed("%s", $response->{msg});
+ }
+
+ if ($response->{msg} =~ /unaltered/) {
+ success("Updating %s: skipped: IP was already set to %s", $host, $response->{ip});
+ } else {
+ success("%s -- Updated successfully to %s", $host, $response->{ip});
+ }
+ }
+}
+
+######################################################################
+## nic_changeip_examples
+######################################################################
+sub nic_changeip_examples {
+ return <<"EoEXAMPLE";
+
+o 'changeip'
+
+The 'changeip' protocol is used by DNS services offered by changeip.com.
+
+Configuration variables applicable to the 'changeip' protocol are:
+ protocol=changeip ##
+ server=fqdn.of.service ## defaults to nic.changeip.com
+ login=service-login ## login name and password registered with the service
+ password=service-password ##
+ fully.qualified.host ## the host registered with the service.
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=changeip, \\
+ login=my-my-changeip.com-login, \\
+ password=my-changeip.com-password \\
+ myhost.changeip.org
+
+EoEXAMPLE
+}
+
+######################################################################
+## nic_changeip_update
+##
+## adapted by Michele Giorato
+##
+## https://nic.ChangeIP.com/nic/update?hostname=host.example.org&myip=66.185.162.19
+##
+######################################################################
+sub nic_changeip_update {
+
+
+ debug("\nnic_changeip_update -------------------");
+
+ ## update each configured host
+ foreach my $h (@_) {
+ my $ip = delete $config{$h}{'wantip'};
+ info("setting IP address to %s for %s", $ip, $h);
+ verbose("UPDATE:", "updating %s", $h);
+
+ my $url;
+ $url = "https://$config{$h}{'server'}/nic/update";
+ $url .= "?hostname=$h";
+ $url .= "&ip=";
+ $url .= $ip if $ip;
+
+ my $reply = geturl(
+ proxy => opt('proxy'),
+ url => $url,
+ login => $config{$h}{'login'},
+ password => $config{$h}{'password'},
+ );
+ if (!defined($reply) || !$reply) {
+ failed("updating %s: Could not connect to %s.", $h, $config{$h}{'server'});
+ next;
+ }
+ next if !header_ok($h, $reply);
+
+ my @reply = split /\n/, $reply;
+ if (grep /success/i, @reply) {
+ $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';
+ warning("SENT: %s", $url) unless opt('verbose');
+ warning("REPLIED: %s", $reply);
+ failed("updating %s: Invalid reply.", $h);
+ }
+ }
+}
+
+######################################################################
+## nic_googledomains_examples
+##
+## written by awalon
+##
+######################################################################
+sub nic_godaddy_examples {
+ return <<"EoEXAMPLE";
+
+o 'godaddy'
+
+The 'godaddy' protocol is used by DNS service offered by https://www.godaddy.com/domains.
+
+Configuration variables applicable to the 'godaddy' protocol are:
+ protocol=godaddy ##
+ login=my-generated-token ## the token/key name provided by the API interface
+ password=my-generated-secret ## the secret provided by the API interface
+ zone=domain.tld ## the domain used for DNS update.
+ ttl=600 ## time to live of the record;
+ hostname.domain.tld ## hostname/subdomain
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=godaddy \\
+ login=my-generated-token \\
+ password=my-generated-secret \\
+ zone=example.com \\
+ hostname.example.com
+
+ ## multiple host update to the DNS service
+ protocol=godaddy \\
+ login=my-generated-token \\
+ password=my-generated-secret \\
+ zone=example.com \\
+ host1.example.com,host2.example.com
+EoEXAMPLE
+}
+######################################################################
+## nic_godaddy_update
+######################################################################
+sub nic_godaddy_update {
+ debug("\nnic_godaddy_update --------------------");
+
+ ## group hosts with identical attributes together
+ my %groups = group_hosts_by([ @_ ], [ qw(server login password zone) ]);
+
+ ## update each set of hosts that had similar configurations
+ foreach my $sig (keys %groups) {
+ my @hosts = @{$groups{$sig}};
+
+ # Update each set configured host.
+ for my $host (@hosts) {
+ my $ip = delete $config{$host}{'wantip'};
+ my $zone = $config{$host}{'zone'};
+ (my $hostname = $host) =~ s/\.\Q$zone\E$//;
+
+ info("%s.%s -- Setting IP address to %s.", $hostname, $zone, $ip);
+ verbose("UPDATE:", "updating %s.%s", $hostname, $zone);
+
+ my $ipversion = is_ipv6($ip) ? "6" : "4";
+ my $rrset_type = $ipversion == "6" ? "AAAA" : "A";
+ my $data = encode_json([{
+ data => $ip,
+ defined($config{$host}{'ttl'}) ? (ttl => $config{$host}{'ttl'}) : (),
+ name => $hostname,
+ type => $rrset_type,
+ }]);
+
+ my $url = "https://$config{$host}{'server'}";
+ $url .= "/${zone}/records/${rrset_type}/${hostname}";
+
+ my $header = "Content-Type: application/json\n";
+ $header .= "Accept: application/json\n";
+ $header .= "Authorization: sso-key $config{$host}{'login'}:$config{$host}{'password'}\n";
+ my $reply = geturl(
+ proxy => opt('proxy'),
+ url => $url,
+ headers => $header,
+ method => 'PUT',
+ data => $data,
+ );
+ unless ($reply) {
+ failed("%s.%s -- Could not connect to %s.", $hostname, $zone, $config{$host}{'server'});
+ next;
+ }
+
+ (my $status) = ($reply =~ m%^s*HTTP/.*\s+(\d+)%i);
+ my $ok = header_ok($host, $reply);
+ my $msg;
+ $reply =~ s/^.*?\n\n//s; # extract payload
+ my $response = eval { decode_json($reply) };
+ if (!defined($response) && $status != "200") {
+ $config{$host}{'status'} = "bad";
+
+ failed("%s.%s -- Unexpected or empty service response, cannot parse data.", $hostname, $zone);
+ } elsif (defined($response->{code})) {
+ verbose("%s.%s -- %s - %s.", $hostname, $zone, $response->{code}, $response->{message});
+ }
+ if ($ok) {
+ # read data
+ $config{$host}{'ip'} = $ip;
+ $config{$host}{'mtime'} = $now;
+ $config{$host}{'status'} = "good";
+
+ success("%s.%s -- Updated successfully to %s (status: %s).", $hostname, $zone, $ip, $status);
+ next;
+ } elsif ($status == "400") {
+ $msg = 'GoDaddy API URL ($url) was malformed.';
+ } elsif ($status == "401") { # authentication error
+ if ($config{$host}{'login'} && $config{$host}{'login'}) {
+ $msg = 'login or password option incorrect.';
+ } else {
+ $msg = 'login or password option missing.';
+ }
+ $msg .= ' Correct values can be obtained from from https://developer.godaddy.com/keys/.';
+ } elsif ($status == "403") {
+ $msg = 'Customer identified by login and password options denied permission.';
+ } elsif ($status == "404") {
+ $msg = "\"${hostname}.${zone}\" not found at GoDaddy, please check zone option and login/password.";
+ } elsif ($status == "422") {
+ $msg = "\"${hostname}.${zone}\" has invalid domain or lacks A/AAAA record.";
+ } elsif ($status == "429") {
+ $msg = 'Too many requests to GoDaddy within brief period.';
+ } elsif ($status == "503") {
+ $msg = "\"${hostname}.${zone}\" is unavailable.";
+ } else {
+ $msg = 'Unexpected service response.';
+ }
+
+ $config{$host}{'status'} = "bad";
+ failed("%s.%s -- %s", $hostname, $zone, $msg);
+ }
+ }
+}
+
+######################################################################
+## nic_googledomains_examples
+##
+## written by Nelson Araujo
+##
+######################################################################
+sub nic_googledomains_examples {
+ return <<"EoEXAMPLE";
+o 'googledomains'
+
+The 'googledomains' protocol is used by DNS service offered by www.google.com/domains.
+
+Configuration variables applicable to the 'googledomains' protocol are:
+ protocol=googledomains ##
+ login=service-login ## the user name provided by the admin interface
+ password=service-password ## the password provided by the admin interface
+ fully.qualified.host ## the host registered with the service.
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=googledomains, \\
+ login=my-generated-user-name, \\
+ password=my-genereated-password \\
+ myhost.com
+
+ ## multiple host update to the custom DNS service
+ protocol=googledomains, \\
+ login=my-generated-user-name, \\
+ password=my-genereated-password \\
+ my-toplevel-domain.com,my-other-domain.com
+EoEXAMPLE
+}
+######################################################################
+## nic_googledomains_update
+######################################################################
+sub nic_googledomains_update {
+ debug("\nnic_googledomains_update -------------------");
+
+ ## group hosts with identical attributes together
+ my %groups = group_hosts_by([ @_ ], [ qw(server login password) ]);
+
+ ## update each set of hosts that had similar configurations
+ foreach my $sig (keys %groups) {
+ my @hosts = @{$groups{$sig}};
+ my $key = $hosts[0];
+ my $ip = $config{$key}{'wantip'};
+
+ # FQDNs
+ for my $host (@hosts) {
+ delete $config{$host}{'wantip'};
+
+ info("setting IP address to %s for %s", $ip, $host);
+ verbose("UPDATE:", "updating %s", $host);
+
+ # Update the DNS record
+ my $url = "https://$config{$host}{'server'}/nic/update";
+ $url .= "?hostname=$host";
+ $url .= "&myip=";
+ $url .= $ip if $ip;
+
+ my $reply = geturl(
+ proxy => opt('proxy'),
+ url => $url,
+ login => $config{$host}{'login'},
+ password => $config{$host}{'password'},
+ );
+ unless ($reply) {
+ failed("updating %s: Could not connect to %s.", $host, $config{$host}{'server'});
+ next;
+ }
+ next if !header_ok($host, $reply);
+
+ # Cache
+ $config{$host}{'ip'} = $ip;
+ $config{$host}{'mtime'} = $now;
+ $config{$host}{'status'} = 'good';
+ }
+ }
+}
+
+######################################################################
+## nic_nsupdate_examples
+######################################################################
+sub nic_nsupdate_examples {
+ return <<"EoEXAMPLE";
+o 'nsupdate'
+
+The 'nsupdate' protocol is used to submit Dynamic DNS Update requests as
+defined in RFC2136 to a name server using the 'nsupdate' command line
+utility part of ISC BIND. Dynamic DNS updates allow resource records to
+be added or removed from a zone configured for dynamic updates through
+DNS requests protected using TSIG. BIND ships with 'ddns-confgen', a
+utility to generate sample configurations and instructions for both the
+server and the client. See nsupdate(1) and ddns-confgen(8) for details.
+
+Configuration variables applicable to the 'nsupdate' protocol are:
+ protocol=nsupdate
+ server=ns1.example.com ## name or IP address of the DNS server to send
+ ## the update requests to; usually master for
+ ## zone, but slaves should forward the request
+ password=tsig.key ## path and name of the symmetric HMAC key file
+ ## to use for TSIG signing of the request
+ ## (as generated by 'ddns-confgen -q' and
+ ## configured on server in 'grant' statement)
+ zone=dyn.example.com ## forward zone that is to be updated
+ ttl=600 ## time to live of the record;
+ ## defaults to 600 seconds
+ tcp=off|on ## nsupdate uses UDP by default, and switches to
+ ## TCP if the update is too large to fit in a
+ ## UDP datagram; this setting forces TCP;
+ ## defaults to off
+ login=/usr/bin/nsupdate ## path and name of nsupdate binary;
+ ## defaults to '/usr/bin/nsupdate'
+ ## fully qualified hostname to update
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=nsupdate \\
+ server=ns1.example.com \\
+ password=/etc/${program}/dyn.example.com.key \\
+ zone=dyn.example.com \\
+ ttl=3600 \\
+ myhost.dyn.example.com
+
+EoEXAMPLE
+}
+
+######################################################################
+## nic_nsupdate_update
+## by Daniel Roethlisberger
+######################################################################
+sub nic_nsupdate_update {
+ debug("\nnic_nsupdate_update -------------------");
+
+ ## group hosts with identical attributes together
+ my %groups = group_hosts_by([ @_ ], [ qw(login password server zone) ]);
+
+ ## update each set of hosts that had similar configurations
+ foreach my $sig (keys %groups) {
+ my @hosts = @{$groups{$sig}};
+ my $hosts = join(',', @hosts);
+ my $h = $hosts[0];
+ my $binary = $config{$h}{'login'};
+ my $keyfile = $config{$h}{'password'};
+ my $server = $config{$h}{'server'};
+ ## nsupdate requires a port number to be separated by whitepace, not colon
+ $server =~ s/:/ /;
+ my $zone = $config{$h}{'zone'};
+ my $ip = $config{$h}{'wantip'};
+ my $recordtype = '';
+ if (is_ipv6($ip)) {
+ $recordtype = 'AAAA';
+ } else {
+ $recordtype = 'A';
+ }
+ delete $config{$_}{'wantip'} foreach @hosts;
+
+ info("setting IP address to %s for %s", $ip, $hosts);
+ verbose("UPDATE:", "updating %s", $hosts);
+
+ ## send separate requests for each zone with all hosts in that zone
+ my $instructions = <<"EoINSTR1";
+server $server
+zone $zone.
+EoINSTR1
+ foreach (@hosts) {
+ $instructions .= <<"EoINSTR2";
+update delete $_. $recordtype
+update add $_. $config{$_}{'ttl'} $recordtype $ip
+EoINSTR2
+ }
+ $instructions .= <<"EoINSTR3";
+send
+EoINSTR3
+ my $command = "$binary -k $keyfile";
+ $command .= " -v" if ynu($config{$h}{'tcp'}, 1, 0, 0);
+ $command .= " -d" if (opt('debug'));
+ verbose("UPDATE:", "nsupdate command is: %s", $command);
+ verbose("UPDATE:", "nsupdate instructions are:\n%s", $instructions);
+
+ my $status = pipecmd($command, $instructions);
+ if ($status eq 1) {
+ foreach (@hosts) {
+ $config{$_}{'ip'} = $ip;
+ $config{$_}{'mtime'} = $now;
+ success("updating %s: %s: IP address set to %s", $_, $status, $ip);
+ }
+ } else {
+ foreach (@hosts) {
+ failed("updating %s", $_);
+ }
+ }
+ }
+}
+
+######################################################################
+
+######################################################################
+## nic_cloudflare_examples
+##
+## written by Ian Pye
+##
+######################################################################
+sub nic_cloudflare_examples {
+ return <<"EoEXAMPLE";
+o 'cloudflare'
+
+The 'cloudflare' protocol is used by DNS service offered by www.cloudflare.com.
+
+Configuration variables applicable to the 'cloudflare' protocol are:
+ protocol=cloudflare ##
+ server=fqdn.of.service ## defaults to api.cloudflare.com/client/v4
+ login=service-login ## login email when using a global API key
+ password=service-password ## Global API key, or an API token. If using an API token, it must have the permissions "Zone - DNS - Edit" and "Zone - Zone - Read". The Zone resources must be "Include - All zones".
+ fully.qualified.host ## the host registered with the service.
+
+Example ${program}.conf file entries:
+ ## single host update using a global API key
+ protocol=cloudflare, \\
+ zone=dns.zone, \\
+ login=my-cloudflare.com-login, \\
+ password=my-cloudflare-global-key \\
+ myhost.com
+
+ ## single host update using an API token
+ protocol=cloudflare, \\
+ zone=dns.zone, \\
+ login=token, \\
+ password=cloudflare-api-token \\
+ myhost.com
+
+ ## multiple host update to the custom DNS service
+ protocol=cloudflare, \\
+ zone=dns.zone, \\
+ login=my-cloudflare.com-login, \\
+ password=my-cloudflare-global-api-key \\
+ my-toplevel-domain.com,my-other-domain.com
+EoEXAMPLE
+}
+######################################################################
+## nic_cloudflare_update
+######################################################################
+sub nic_cloudflare_update {
+ debug("\nnic_cloudflare_update -------------------");
+
+ ## group hosts with identical attributes together
+ my %groups = group_hosts_by([ @_ ], [ qw(ssh login password server wildcard mx backupmx zone) ]);
+
+ ## update each set of hosts that had similar configurations
+ foreach my $sig (keys %groups) {
+ my @hosts = @{$groups{$sig}};
+ my $hosts = join(',', @hosts);
+ my $key = $hosts[0];
+
+ my $headers = "Content-Type: application/json\n";
+ if ($config{$key}{'login'} eq 'token') {
+ $headers .= "Authorization: Bearer $config{$key}{'password'}\n";
+ } else {
+ $headers .= "X-Auth-Email: $config{$key}{'login'}\n";
+ $headers .= "X-Auth-Key: $config{$key}{'password'}\n";
+ }
+
+ # FQDNs
+ for my $domain (@hosts) {
+ my $ipv4 = delete $config{$domain}{'wantipv4'};
+ my $ipv6 = delete $config{$domain}{'wantipv6'};
+
+ info("getting Cloudflare Zone ID for %s", $domain);
+
+ # Get zone ID
+ my $url = "https://$config{$key}{'server'}/zones/?";
+ $url .= "name=" . $config{$key}{'zone'};
+
+ my $reply = geturl(proxy => opt('proxy'),
+ url => $url,
+ headers => $headers
+ );
+ unless ($reply && header_ok($domain, $reply)) {
+ failed("updating %s: Could not connect to %s.", $domain, $config{$key}{'server'});
+ next;
+ }
+
+ # Strip header
+ $reply =~ qr/{(?:[^{}]*|(?R))*}/mp;
+ my $response = eval {decode_json(${^MATCH})};
+ unless ($response && $response->{result}) {
+ failed("updating %s: invalid json or result.", $domain);
+ next;
+ }
+
+ # Pull the ID out of the json, messy
+ my ($zone_id) = map {$_->{name} eq $config{$key}{'zone'} ? $_->{id} : ()} @{$response->{result}};
+ unless ($zone_id) {
+ failed("updating %s: No zone ID found.", $config{$key}{'zone'});
+ next;
+ }
+ info("Zone ID is %s", $zone_id);
+
+
+ # IPv4 and IPv6 handling are similar enough to do in a loop...
+ foreach my $ip ($ipv4, $ipv6) {
+ next if (!$ip);
+ my $ipv = ($ip eq ($ipv6 // '')) ? '6' : '4';
+ my $type = ($ip eq ($ipv6 // '')) ? 'AAAA' : 'A';
+
+ info("updating %s: setting IPv$ipv address to %s", $domain, $ip);
+ $config{$domain}{"status-ipv$ipv"} = 'failed';
+
+ # Get DNS 'A' or 'AAAA' record ID
+ $url = "https://$config{$key}{'server'}/zones/$zone_id/dns_records?";
+ $url .= "type=$type&name=$domain";
+ $reply = geturl(proxy => opt('proxy'),
+ url => $url,
+ headers => $headers
+ );
+ unless ($reply && header_ok($domain, $reply)) {
+ failed("updating %s: Could not connect to %s.", $domain, $config{$key}{'server'});
+ next;
+ }
+ # Strip header
+ $reply =~ qr/{(?:[^{}]*|(?R))*}/mp;
+ $response = eval {decode_json(${^MATCH})};
+ unless ($response && $response->{result}) {
+ failed("updating %s: invalid json or result.", $domain);
+ next;
+ }
+ # Pull the ID out of the json, messy
+ my ($dns_rec_id) = map {$_->{name} eq $domain ? $_->{id} : ()} @{$response->{result}};
+ unless($dns_rec_id) {
+ failed("updating %s: Cannot set IPv$ipv to %s No '$type' record at Cloudflare", $domain, $ip);
+ next;
+ }
+ debug("updating %s: DNS '$type' record ID: $dns_rec_id", $domain);
+ # Set domain
+ $url = "https://$config{$key}{'server'}/zones/$zone_id/dns_records/$dns_rec_id";
+ my $data = "{\"content\":\"$ip\"}";
+ $reply = geturl(proxy => opt('proxy'),
+ url => $url,
+ headers => $headers,
+ method => "PATCH",
+ data => $data
+ );
+ unless ($reply && header_ok($domain, $reply)) {
+ failed("updating %s: Could not connect to %s.", $domain, $config{$domain}{'server'});
+ next;
+ }
+ # Strip header
+ $reply =~ qr/{(?:[^{}]*|(?R))*}/mp;
+ $response = eval {decode_json(${^MATCH})};
+ if ($response && $response->{result}) {
+ success("updating %s: IPv$ipv address set to %s", $domain, $ip);
+ $config{$domain}{"ipv$ipv"} = $ip;
+ $config{$domain}{'mtime'} = $now;
+ $config{$domain}{"status-ipv$ipv"} = 'good';
+ } else {
+ failed("updating %s: invalid json or result.", $domain);
+ }
+ }
+ }
+ }
+}
+
+######################################################################
+## nic_hetzner_examples
+##
+## written by Joerg Werner
+##
+######################################################################
+sub nic_hetzner_examples {
+ return <<"EoEXAMPLE";
+o 'hetzner'
+
+The 'hetzner' protocol is used by DNS service offered by www.hetzner.com.
+
+Configuration variables applicable to the 'hetzner' protocol are:
+ protocol=hetzner ##
+ server=fqdn.of.service ## can be omitted, defaults to dns.hetzner.com/api/v1
+ password=service-password ## API token
+ fully.qualified.host ## the host registered with the service.
+
+Example ${program}.conf file entries:
+ protocol=hetzner, \\
+ zone=dns.zone, \\
+ password=my-hetzner-api-token \\
+ my-toplevel-domain.com,my-other-domain.com
+EoEXAMPLE
+}
+######################################################################
+## nic_hetzner_update
+######################################################################
+sub nic_hetzner_update {
+ debug("\nnic_hetzner_update -------------------");
+
+ ## group hosts with identical attributes together
+ my %groups = group_hosts_by([ @_ ], [ qw(ssh login password server wildcard mx backupmx zone) ]);
+
+ ## update each set of hosts that had similar configurations
+ foreach my $sig (keys %groups) {
+ my @hosts = @{$groups{$sig}};
+ my $hosts = join(',', @hosts);
+ my $key = $hosts[0];
+
+ my $headers = "Auth-API-Token: $config{$key}{'password'}\n";
+ $headers .= "Content-Type: application/json";
+
+ # FQDNs
+ for my $domain (@hosts) {
+ (my $hostname = $domain) =~ s/\.$config{$key}{zone}$//;
+ my $ipv4 = delete $config{$domain}{'wantipv4'};
+ my $ipv6 = delete $config{$domain}{'wantipv6'};
+
+ info("getting Hetzner Zone ID for %s", $domain);
+
+ # Get zone ID
+ my $url = "https://$config{$key}{'server'}/zones?name=" . $config{$key}{'zone'};
+
+ my $reply = geturl(proxy => opt('proxy'),
+ url => $url,
+ headers => $headers
+ );
+ unless ($reply && header_ok($domain, $reply)) {
+ failed("updating %s: Could not connect to %s.", $domain, $config{$key}{'server'});
+ next;
+ }
+
+ # Strip header
+ $reply =~ qr/{(?:[^{}]*|(?R))*}/mp;
+ my $response = eval {decode_json(${^MATCH})};
+ unless ($response && $response->{zones}) {
+ failed("updating %s: invalid json or result.", $domain);
+ next;
+ }
+
+ # Pull the ID out of the json, messy
+ my ($zone_id) = map {$_->{name} eq $config{$key}{'zone'} ? $_->{id} : ()} @{$response->{zones}};
+ unless ($zone_id) {
+ failed("updating %s: No zone ID found.", $config{$key}{'zone'});
+ next;
+ }
+ info("Zone ID is %s", $zone_id);
+
+
+ # IPv4 and IPv6 handling are similar enough to do in a loop...
+ foreach my $ip ($ipv4, $ipv6) {
+ next if (!$ip);
+ my $ipv = ($ip eq ($ipv6 // '')) ? '6' : '4';
+ my $type = ($ip eq ($ipv6 // '')) ? 'AAAA' : 'A';
+
+ info("updating %s: setting IPv$ipv address to %s", $domain, $ip);
+ $config{$domain}{"status-ipv$ipv"} = 'failed';
+
+ # Get DNS 'A' or 'AAAA' record ID
+ $url = "https://$config{$key}{'server'}/records?$zone_id";
+ $reply = geturl(proxy => opt('proxy'),
+ url => $url,
+ headers => $headers
+ );
+ unless ($reply && header_ok($domain, $reply)) {
+ failed("updating %s: Could not connect to %s.", $domain, $config{$key}{'server'});
+ next;
+ }
+ # Strip header
+ $reply =~ qr/{(?:[^{}]*|(?R))*}/mp;
+ $response = eval {decode_json(${^MATCH})};
+ unless ($response && $response->{records}) {
+ failed("updating %s: invalid json or result.", $domain);
+ next;
+ }
+ # Pull the ID out of the json, messy
+ my ($dns_rec_id) = map { ($_->{name} eq $hostname && $_->{type} eq $type) ? $_->{id} : ()} @{$response->{records}};
+
+ # Set domain
+ my $http_method="";
+ if ($dns_rec_id)
+ {
+ debug("updating %s: DNS '$type' record ID: $dns_rec_id", $domain);
+ $url = "https://$config{$key}{'server'}/records/$dns_rec_id";
+ $http_method = "PUT";
+ } else {
+ debug("creating %s: DNS '$type'", $domain);
+ $url = "https://$config{$key}{'server'}/records";
+ $http_method = "POST";
+ }
+ my $data = "{\"zone_id\":\"$zone_id\", \"name\": \"$hostname\", \"value\": \"$ip\", \"type\": \"$type\", \"ttl\": $config{$domain}{'ttl'}}";
+
+ $reply = geturl(proxy => opt('proxy'),
+ url => $url,
+ headers => $headers,
+ method => $http_method,
+ data => $data
+ );
+ unless ($reply && header_ok($domain, $reply)) {
+ failed("updating %s: Could not connect to %s.", $domain, $config{$domain}{'server'});
+ next;
+ }
+ # Strip header
+ $reply =~ qr/{(?:[^{}]*|(?R))*}/mp;
+ $response = eval {decode_json(${^MATCH})};
+ if ($response && $response->{record}) {
+ success("updating %s: IPv$ipv address set to %s", $domain, $ip);
+ $config{$domain}{"ipv$ipv"} = $ip;
+ $config{$domain}{'mtime'} = $now;
+ $config{$domain}{"status-ipv$ipv"} = 'good';
+ } else {
+ failed("updating %s: invalid json or result.", $domain);
+ }
+ }
+ }
+ }
+}
+
+######################################################################
+## nic_yandex_examples
+######################################################################
+sub nic_yandex_examples {
+ return <<"EoEXAMPLE";
+o Yandex
+
+The 'yandex' protocol is used to by DNS service offered by Yandex.
+
+Configuration variables applicable to the 'yandex' protocol are:
+ protocol=yandex ##
+ server=fqdn.of.service ## defaults to pddimp.yandex.ru
+ login=dns.zone ## Your zone name
+ password=pdd-token ## PDD token for authentication
+ fully.qualified.host ## the host registered with the service.
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=yandex, \\
+ login=myhost.com, \\
+ password=123456789ABCDEF0000000000000000000000000000000000000 \\
+ record.myhost.com
+
+ ## multiple host update
+ protocol=yandex, \\
+ login=myhost.com, \\
+ password=123456789ABCDEF0000000000000000000000000000000000000 \\
+ record.myhost.com,other.myhost.com
+EoEXAMPLE
+}
+######################################################################
+## nic_yandex_update
+##
+## written by Denis Akimkin
+##
+######################################################################
+sub nic_yandex_update {
+ debug("\nnic_yandex_update -------------------");
+
+ ## group hosts with identical attributes together
+ my %groups = group_hosts_by([ @_ ], [ qw(server login pasword) ]);
+
+ ## update each set of hosts that had similar configurations
+ foreach my $sig (keys %groups) {
+ my @hosts = @{$groups{$sig}};
+ my $key = $hosts[0];
+ my $ip = $config{$key}{'wantip'};
+ my $headers = "PddToken: $config{$key}{'password'}\n";
+
+ # FQDNs
+ for my $host (@hosts) {
+ delete $config{$host}{'wantip'};
+
+ info("setting IP address to %s for %s", $ip, $host);
+ verbose("UPDATE:", "updating %s", $host);
+
+ # Get record ID for host
+ my $url = "https://$config{$host}{'server'}/api2/admin/dns/list?";
+ $url .= "domain=";
+ $url .= $config{$key}{'login'};
+ my $reply = geturl(proxy => opt('proxy'), url => $url, headers => $headers);
+ unless ($reply) {
+ failed("updating %s: Could not connect to %s.", $host, $config{$key}{'server'});
+ next;
+ }
+ next if !header_ok($host, $reply);
+
+ # Strip header
+ $reply =~ s/^.*?\n\n//s;
+ my $response = eval { decode_json($reply) };
+ if ($response->{success} eq 'error') {
+ failed("%s", $response->{error});
+ next;
+ }
+
+ # Pull the ID out of the json
+ my ($id) = map { $_->{fqdn} eq $host ? $_->{record_id} : () } @{$response->{records}};
+ unless ($id) {
+ failed("updating %s: DNS record ID not found.", $host);
+ next;
+ }
+
+ # Update the DNS record
+ $url = "https://$config{$host}{'server'}/api2/admin/dns/edit";
+ my $data = "domain=";
+ $data .= $config{$key}{'login'};
+ $data .= "&record_id=";
+ $data .= $id;
+ $data .= "&content=";
+ $data .= $ip if $ip;
+
+ $reply = geturl(
+ proxy => opt('proxy'),
+ url => $url,
+ headers => $headers,
+ method => 'POST',
+ data => $data,
+ );
+ unless ($reply) {
+ failed("updating %s: Could not connect to %s.", $host, $config{$host}{'server'});
+ next;
+ }
+ next if !header_ok($host, $reply);
+
+ # Strip header
+ $reply =~ s/^.*?\n\n//s;
+ $response = eval { decode_json($reply) };
+ if ($response->{success} eq 'error') {
+ failed("%s", $response->{error});
+ } else {
+ success("%s -- Updated Successfully to %s", $host, $ip);
+ }
+
+ # Cache
+ $config{$host}{'ip'} = $ip;
+ $config{$host}{'mtime'} = $now;
+ $config{$host}{'status'} = 'good';
+ }
+ }
+}
+
+######################################################################
+## nic_duckdns_examples
+######################################################################
+sub nic_duckdns_examples {
+ return <<"EoEXAMPLE";
+o 'duckdns'
+
+The 'duckdns' protocol is used by the free
+dynamic DNS service offered by www.duckdns.org.
+Check https://www.duckdns.org/install.jsp?tab=linux-cron for API
+
+Configuration variables applicable to the 'duckdns' protocol are:
+ protocol=duckdns ##
+ server=www.fqdn.of.service ## defaults to www.duckdns.org
+ password=service-password ## password (token) registered with the service
+ non-fully.qualified.host ## the host registered with the service.
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=duckdns, \\
+ password=your_password, \\
+ myhost
+
+EoEXAMPLE
+}
+
+######################################################################
+## nic_duckdns_update
+## by George Kranis (copypasta from nic_dtdns_update)
+## https://www.duckdns.org/update?domains=mydomain1,mydomain2&token=xxxx-xxx-xx-x&ip=x.x.x.x
+## response contains OK or KO
+######################################################################
+sub nic_duckdns_update {
+ debug("\nnic_duckdns_update -------------------");
+
+ ## update each configured host
+ ## should improve to update in one pass
+ foreach my $h (@_) {
+ my $ip = delete $config{$h}{'wantip'};
+ info("setting IP address to %s for %s", $ip, $h);
+ verbose("UPDATE:", "updating %s", $h);
+
+ # Set the URL that we're going to to update
+ my $url;
+ $url = "https://$config{$h}{'server'}/update";
+ $url .= "?domains=";
+ $url .= $h;
+ $url .= "&token=";
+ $url .= $config{$h}{'password'};
+ $url .= "&ip=";
+ $url .= $ip;
+
+
+ # Try to get URL
+ my $reply = geturl(proxy => opt('proxy'), url => $url);
+
+ # No response, declare as failed
+ if (!defined($reply) || !$reply) {
+ failed("updating %s: Could not connect to %s.", $h, $config{$h}{'server'});
+ next;
+ }
+ next if !header_ok($h, $reply);
+
+ my @reply = split /\n/, $reply;
+ my $returned = pop(@reply);
+ if ($returned =~ /OK/) {
+ $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';
+ failed("updating %s: Server said: '%s'", $h, $returned);
+ }
+ }
+}
+
+######################################################################
+## nic_freemyip_examples
+######################################################################
+sub nic_freemyip_examples {
+ return <<"EoEXAMPLE";
+o 'freemyip'
+
+The 'freemyip' protocol is used by the free
+dynamic DNS service available at freemyip.com.
+API is documented here: https://freemyip.com/help.py
+
+Configuration variables applicable to the 'freemyip' protocol are:
+ protocol=freemyip ##
+ password=service-token ## token for your domain
+ non-fully.qualified.host ## the host registered with the service.
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=freemyip, \\
+ password=35a6b8d65c6e67c7f78cca65cd \\
+ myhost
+
+EoEXAMPLE
+}
+
+######################################################################
+## nic_freemyip_update
+## by Cadence (reused code from nic_duckdns)
+## http://freemyip.com/update?token=ec54b4b64db27fe8873c7f7&domain=myhost
+## response contains OK or ERROR
+######################################################################
+sub nic_freemyip_update {
+ debug("\nnic_freemyip_update -------------------");
+
+ foreach my $h (@_) {
+ my $ip = delete $config{$h}{'wantip'};
+ info("setting IP address to %s for %s", $ip, $h);
+ verbose("UPDATE:", "updating %s", $h);
+
+ # Set the URL that we're going to to update
+ my $url;
+ $url = "https://$config{$h}{'server'}/update";
+ $url .= "?token=";
+ $url .= $config{$h}{'password'};
+ $url .= "&domain=";
+ $url .= $h;
+
+ # Try to get URL
+ my $reply = geturl(proxy => opt('proxy'), url => $url);
+
+ # No response, declare as failed
+ if (!defined($reply) || !$reply) {
+ failed("updating %s: Could not connect to %s.", $h, $config{$h}{'server'});
+ next;
+ }
+ next if !header_ok($h, $reply);
+
+ my @reply = split /\n/, $reply;
+ my $returned = pop(@reply);
+ if ($returned =~ /OK/) {
+ $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';
+ failed("updating %s: Server said: '%s'", $h, $returned);
+ }
+ }
+}
+
+######################################################################
+## nic_woima_examples
+######################################################################
+sub nic_woima_examples {
+ return <<"EoEXAMPLE";
+o 'woima'
+
+The 'woima' protocol is used by the free
+dynamic DNS service offered by woima.fi.
+It offers also nameservers for own domains for free.
+Dynamic DNS service for own domains is not free.
+
+Configuration variables applicable to the 'woima' protocol are:
+ protocol=woima ##
+ server=fqdn.of.service ## defaults to dyn.woima.fi
+ script=/path/to/script ## defaults to /nic/update
+ backupmx=no|yes ## indicates that this host is the primary MX for the domain.
+ static=no|yes ## indicates that this host has a static IP address.
+ custom=no|yes ## indicates that this host is a 'custom' top-level domain name.
+ mx=any.host.domain ## a host MX'ing for this host definition.
+ wildcard=no|yes ## add a DNS wildcard CNAME record that points to {host}
+ login=service-login ## login name and password registered with the service
+ password=service-password ##
+ fully.qualified.host ## the host registered with the service.
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=woima, \\
+ login=my-dyndns.org-login, \\
+ password=my-dyndns.org-password \\
+ myhost.dyndns.org
+
+ ## multiple host update with wildcard'ing mx, and backupmx
+ protocol=woima, \\
+ login=my-dyndns.org-login, \\
+ password=my-dyndns.org-password, \\
+ mx=a.host.willing.to.mx.for.me,backupmx=yes,wildcard=yes \\
+ myhost.dyndns.org,my2ndhost.dyndns.org
+
+ ## multiple host update to the custom DNS service
+ protocol=woima, \\
+ login=my-dyndns.org-login, \\
+ password=my-dyndns.org-password \\
+ my-toplevel-domain.com,my-other-domain.com
+EoEXAMPLE
+}
+######################################################################
+## nic_woima_update
+######################################################################
+sub nic_woima_update {
+ debug("\nnic_woima_update -------------------");
+
+ my %errors = (
+ 'badauth' => 'Bad authorization (username or password)',
+ 'badsys' => 'The system parameter given was not valid',
+
+ 'notfqdn' => 'A Fully-Qualified Domain Name was not provided',
+ 'nohost' => 'The hostname specified does not exist in the database',
+ '!yours' => 'The hostname specified exists, but not under the username currently being used',
+ '!donator' => 'The offline setting was set, when the user is not a donator',
+ '!active' => 'The hostname specified is in a Custom DNS domain which has not yet been activated.',
+ 'abuse', => 'The hostname specified is blocked for abuse; you should receive an email notification ' .
+ 'which provides an unblock request link. More info can be found on ' .
+ 'https://www.dyndns.com/support/abuse.html',
+
+ 'numhost' => 'System error: Too many or too few hosts found. Contact support@dyndns.org',
+ 'dnserr' => 'System error: DNS error encountered. Contact support@dyndns.org',
+
+ 'nochg' => 'No update required; unnecessary attempts to change to the current address are considered abusive',
+ );
+
+ for my $h (@_) {
+ my $ip = $config{$h}{'wantip'};
+ delete $config{$h}{'wantip'};
+
+ info("setting IP address to %s for %s", $ip, $h);
+ verbose("UPDATE:", "updating %s", $h);
+
+ ## Select the DynDNS system to update
+ ## TODO: endpoint does not support https with functioning certificate. Remove?
+ my $url = "http://$config{$h}{'server'}$config{$h}{'script'}?system=";
+ if ($config{$h}{'custom'}) {
+ warning("updating %s: 'custom' and 'static' may not be used together. ('static' ignored)", $h)
+ if $config{$h}{'static'};
+ $url .= 'custom';
+
+ } elsif ($config{$h}{'static'}) {
+ $url .= 'statdns';
+
+ } else {
+ $url .= 'dyndns';
+ }
+
+ $url .= "&hostname=$h";
+ $url .= "&myip=";
+ $url .= $ip if $ip;
+
+ ## some args are not valid for a custom domain.
+ $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');
+ }
+
+ my $reply = geturl(
+ proxy => opt('proxy'),
+ url => $url,
+ login => $config{$h}{'login'},
+ password => $config{$h}{'password'},
+ );
+ if (!defined($reply) || !$reply) {
+ failed("updating %s: Could not connect to %s.", $h, $config{$h}{'server'});
+ next;
+ }
+ next if !header_ok($h, $reply);
+
+ my @reply = split /\n/, $reply;
+ my $state = 'header';
+ my $returnedip = $ip;
+
+ foreach my $line (@reply) {
+ if ($state eq 'header') {
+ $state = 'body';
+
+ } elsif ($state eq 'body') {
+ $state = 'results' if $line eq '';
+
+ } elsif ($state =~ /^results/) {
+ $state = 'results2';
+
+ # bug #10: some dyndns providers does not return the IP so
+ # we can't use the returned IP
+ my ($status, $returnedip) = split / /, lc $line;
+ $ip = $returnedip if (not $ip);
+
+ $config{$h}{'status'} = $status;
+ if ($status eq 'good') {
+ $config{$h}{'ip'} = $ip;
+ $config{$h}{'mtime'} = $now;
+ success("updating %s: %s: IP address set to %s", $h, $status, $ip);
+
+ } elsif (exists $errors{$status}) {
+ if ($status eq 'nochg') {
+ warning("updating %s: %s: %s", $h, $status, $errors{$status});
+ $config{$h}{'ip'} = $ip;
+ $config{$h}{'mtime'} = $now;
+ $config{$h}{'status'} = 'good';
+
+ } else {
+ failed("updating %s: %s: %s", $h, $status, $errors{$status});
+ }
+
+ } elsif ($status =~ /w(\d+)(.)/) {
+ my ($wait, $units) = ($1, lc $2);
+ my ($sec, $scale) = ($wait, 1);
+
+ ($scale, $units) = (1, 'seconds') if $units eq 's';
+ ($scale, $units) = (60, 'minutes') if $units eq 'm';
+ ($scale, $units) = (60*60, 'hours') if $units eq 'h';
+
+ $sec = $wait * $scale;
+ $config{$h}{'wtime'} = $now + $sec;
+ warning("updating %s: %s: wait %s %s before further updates", $h, $status, $wait, $units);
+
+ } else {
+ failed("updating %s: unexpected status (%s)", $h, $line);
+ }
+ }
+ }
+ failed("updating %s: Could not connect to %s.", $h, $config{$h}{'server'})
+ if $state ne 'results2';
+ }
+}
+
+######################################################################
+## nic_dondominio_examples
+######################################################################
+sub nic_dondominio_examples {
+ return <<"EoEXAMPLE";
+o 'dondominio'
+The 'dondominio' protocol is used by DNS service offered by www.dondominio.com/ .
+API information and user instructions available at: https://dev.dondominio.com/dondns/docs/api/
+Configuration variables applicable to the 'dondominio' protocol are:
+ protocol=dondominio ##
+ login=service-login ## the username registered with the service
+ password=dondominio-apikey ## API key provided by dondominio -see link above-
+ fully.qualified.host ## the host registered with the service.
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=dondominio, \\
+ login=my-generated-user-name, \\
+ password=dondominio-apikey \\
+ myhost.tld
+
+EoEXAMPLE
+}
+
+######################################################################
+## nic_dondominio_examples
+######################################################################
+
+sub nic_dondominio_update {
+ debug("\nnic_dondominio_update -------------------");
+
+ ## update each configured host
+ ## should improve to update in one pass
+ foreach my $h (@_) {
+ my $ip = delete $config{$h}{'wantip'};
+ info("setting IP address to %s for %s", $ip, $h);
+ verbose("UPDATE:", "updating %s", $h);
+
+ # Set the URL that we're going to update
+ my $url;
+ $url = "https://$config{$h}{'server'}/plain/";
+ $url .= "?user=";
+ $url .= $config{$h}{'login'};
+ $url .= "&password=";
+ $url .= $config{$h}{'password'};
+ $url .= "&host=";
+ $url .= $h;
+ $url .= "&ip=";
+ $url .= $ip if $ip;
+
+
+ # Try to get URL
+ my $reply = geturl(proxy => opt('proxy'), url => $url);
+
+ # No response, declare as failed
+ if (!defined($reply) || !$reply) {
+ failed("updating %s: Could not connect to %s.", $h, $config{$h}{'server'});
+ next;
+ }
+ next if !header_ok($h, $reply);
+
+ my @reply = split /\n/, $reply;
+ my $returned = pop(@reply);
+ if ($returned =~ /OK/) {
+ $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';
+ failed("updating %s: Server said: '%s'", $h, $returned);
+ }
+ }
+}
+
+######################################################################
+## nic_dnsmadeeasy_examples
+######################################################################
+sub nic_dnsmadeeasy_examples {
+ return <<"EoEXAMPLE";
+o 'dnsmadeeasy'
+
+The 'dnsmadeeasy' protocol is used by the DNS Made Easy service at https://www.dnsmadeeasy.com.
+API is documented here: https://dnsmadeeasy.com/technology/dynamic-dns/
+
+Configuration variables applicable to the 'dnsmadeeasy' protocol are:
+ protocol=dnsmadeeasy ##
+ login=email-address ## Email address used to log in to your account.
+ password=dynamic-record-password ## Generated password for your dynamic DNS record.
+ record-id-1,record-id-2,... ## Numeric dynamic DNS record IDs, comma-separated if updating multiple.
+
+Note: Dynamic record ID is generated when you create a new Dynamic DNS record in the DNS Made Easy control panel.
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=dnsmadeeasy, \\
+ username=dme\@example.com, \\
+ password=myg3nerat3dp4ssword, \\
+ 1007,1008
+
+EoEXAMPLE
+}
+
+######################################################################
+## nic_dnsmadeeasy_update
+######################################################################
+sub nic_dnsmadeeasy_update {
+ debug("\nnic_dnsmadeeasy_update -------------------");
+
+ my %messages = (
+ 'error-auth' => 'Invalid username or password, or invalid IP syntax',
+ 'error-auth-suspend' => 'User has had their account suspended due to complaints or misuse of the service.',
+ 'error-auth-voided' => 'User has had their account permanently revoked.',
+ 'error-record-invalid' =>'Record ID number does not exist in the system.',
+ 'error-record-auth' => 'User does not have access to this record.',
+ 'error-record-ip-same' => 'No update required.',
+ 'error-system' => 'General system error which is caught and recognized by the system.',
+ 'error' => 'General system error unrecognized by the system.',
+ 'success' => 'Record successfully updated!',
+ );
+
+ ## update each configured host
+ ## should improve to update in one pass
+ foreach my $h (@_) {
+ my $ip = delete $config{$h}{'wantip'};
+ info("Setting IP address to %s for %s", $ip, $h);
+ verbose("UPDATE:", "Updating %s", $h);
+
+ # Set the URL that we're going to to update
+ my $url;
+ $url = $globals{'ssl'} ? "https://" : "http://";
+ $url .= $config{$h}{'server'} . $config{$h}{'script'};
+ $url .= "?username=$config{$h}{'login'}";
+ $url .= "&password=$config{$h}{'password'}";
+ $url .= "&ip=$ip";
+ $url .= "&id=$h";
+
+ # Try to get URL
+ my $reply = geturl(proxy => opt('proxy'), url => $url);
+
+ # No response, declare as failed
+ if (!defined($reply) || !$reply) {
+ failed("Updating %s: Could not connect to %s.", $h, $config{$h}{'server'});
+ next;
+ }
+ next if !header_ok($h, $reply);
+
+ my @reply = split /\n/, $reply;
+ my $returned = pop(@reply);
+ if ($returned =~ 'success') {
+ $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';
+ failed("Updating %s: Server said: '%s': %s", $h, $returned, $messages{$returned});
+ }
+ }
+}
+
+######################################################################
+## nic_ovh_examples
+######################################################################
+sub nic_ovh_examples {
+ return <<"EoEXAMPLE";
+
+o 'ovh'
+
+The 'ovh' protocol is used by DNS services offered by www.ovh.com.
+
+API information and user instructions available at: https://docs.ovh.com/gb/en/domains/hosting_dynhost/
+
+Configuration variables applicable to the 'ovh' protocol are:
+ protocol=ovh ##
+ login=dnsdomain-userid ## The username/id registered with the service
+ password=userid-password ## The password related to the username/id
+ fully.qualified.host ## the hostiname registered with the service.
+
+Example ${program}.conf file entries:
+ ## single host update
+ protocol=ovh, \\
+ login=example.com-dynhostuser, \\
+ password=your_password, \\
+ test.example.com
+
+EoEXAMPLE
+}
+
+######################################################################
+## nic_ovh_update
+######################################################################
+sub nic_ovh_update {
+ debug("\nnic_ovh_update -------------------");
+
+ ## update each configured host
+ ## should improve to update in one pass
+ foreach my $h (@_) {
+ my $ip = delete $config{$h}{'wantip'};
+ info("setting IP address to %s for %s", $ip, $h);
+ verbose("UPDATE:","updating %s", $h);
+
+ # Set the URL that we're going to update
+ my $url;
+ $url .= "https://$config{$h}{'server'}$config{$h}{'script'}?system=dyndns";
+ $url .= "&hostname=$h";
+ $url .= "&myip=";
+ $url .= $ip if $ip;
+
+ my $reply = geturl(
+ proxy => opt('proxy'),
+ url => $url,
+ login => $config{$h}{'login'},
+ password => $config{$h}{'password'},
+ );
+
+ if (!defined($reply) || !$reply) {
+ failed("updating %s: Could not connect to %s.", $h, $config{$h}{'server'});
+ next;
+ }
+
+ my @reply = split /\n/, $reply;
+ my $returned = pop(@reply);
+ if ($returned =~ /good/ || $returned =~ /nochg/) {
+ $config{$h}{'ip'} = $ip;
+ $config{$h}{'mtime'} = $now;
+ $config{$h}{'status'} = 'good';
+ if ($returned =~ /good/) {
+ success("updating %s: good: IP address set to %s", $h, $ip);
+ } else {
+ success("updating %s: skipped: IP address was already set to %s.", $h, $ip);
+ }
+ } else {
+ $config{$h}{'status'} = 'failed';
+ failed("updating %s: Server said: '%s'", $h, $returned);
+ }
+ }
+}
+
+sub nic_cloudns_examples {
+ return <<"EoEXAMPLE";
+o 'cloudns'
+
+The 'cloudns' protocol is used for ClouDNS (https://www.cloudns.net). Details
+about dynamic DNS updates can be found at https://www.cloudns.net/dynamic-dns/.
+
+Available configuration variables:
+ * dynurl: The DynURL associated with the A or AAAA record you wish to update.
+
+Limitations:
+ * $program cannot tell if the DynURL you provide belongs to the hostname you
+ specify.
+ * ClouDNS does not document how to tell whether an update suceeded or failed,
+ so there is no way for $program to reliably handle failures.
+ * The ClouDNS API does not provide a reliable way to set the desired IP
+ address. It might save the IP address you want, or it might save the IP
+ address that connects to CloudDNS. It is more likely to work if you do not
+ use a proxy.
+
+Example ${program}.conf file entry:
+ protocol=cloudns, \\
+ dynurl=https://ipv4.cloudns.net/api/dynamicURL/?q=Njc1OTE2OjY3Njk0ND..., \\
+ myhost.example.com
+EoEXAMPLE
+}
+
+sub nic_cloudns_update {
+ my %groups = group_hosts_by([ @_ ], [ qw(dynurl) ]);
+ for my $hr (values(%groups)) {
+ my @hosts = @$hr;
+ my $hosts = join(',', @hosts);
+ my $ip = $config{$hosts[0]}{'wantip'};
+ my $dynurl = $config{$hosts[0]}{'dynurl'};
+ delete $config{$_}{'wantip'} for @hosts;
+ # https://www.cloudns.net/wiki/article/36/ says, "If you are behind a proxy and your real
+ # IP is set in the header X-Forwarded-For you need to add &proxy=1 at the end of the
+ # DynamicURL." We abuse this to pass the desired IP address to ClouDNS, which might not be
+ # the same as the client IP address seen by ClouDNS.
+ my $reply = geturl(
+ proxy => opt('proxy'),
+ url => $dynurl . '&proxy=1',
+ headers => "X-Forwarded-For: $ip\n",
+ );
+ if (($reply // '') eq '' || !header_ok($hosts, $reply)) {
+ $config{$_}{'status'} = 'failed' for @hosts;
+ failed("updating %s: failed to visit DynURL", $hosts);
+ next;
+ }
+ $reply =~ s/^.*?\n\n//s; # Strip the headers.
+ chomp($reply);
+ if ($reply eq "The record's key is wrong!" || $reply eq "Invalid request.") {
+ $config{$_}{'status'} = 'failed' for @hosts;
+ failed("updating %s: %s", $hosts, $reply);
+ next;
+ }
+ # There's no documentation explaining possible return values, so we assume success.
+ $config{$_}{'ip'} = $ip for @hosts;
+ $config{$_}{'mtime'} = $now for @hosts;
+ $config{$_}{'status'} = 'good' for @hosts;
+ success("updating %s: IP address set to %s", $hosts, $ip);
+ }
+}
+
+######################################################################
+## nic_dinahosting_examples
+######################################################################
+sub nic_dinahosting_examples {
+ return <<"EoEXAMPLE";
+o 'dinahosting'
+
+The 'dinahosting' protocol is used by dinahosting (https://dinahosting.com).
+Details about the API can be found at https://dinahosting.com/api.
+
+Available configuration variables and their defaults:
+ * login (required) is your username.
+ * password (required) is your password.
+ * server=dinahosting.com is the hostname part of the dinahosting API URL.
+ * script=/special/api.php is the path part of the dinahosting API URL.
+
+Example ${program}.conf file entry:
+ protocol=dinahosting, \\
+ login=myusername, \\
+ password=mypassword \\
+ myhost.mydomain.com
+EoEXAMPLE
+}
+
+######################################################################
+## nic_dinahosting_update
+######################################################################
+sub nic_dinahosting_update {
+ debug("\nnic_dinahosting_update -------------------");
+ for my $h (@_) {
+ my $ip = delete $config{$h}{'wantip'};
+ info("setting IP address to %s for %s", $ip, $h);
+ verbose("UPDATE:", "updating %s", $h);
+ my ($hostname, $domain) = split(/\./, $h, 2);
+ my $url = "https://$config{$h}{'server'}$config{$h}{'script'}";
+ $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'},
+ url => $url,
+ );
+ $config{$h}{'status'} = 'failed'; # assume failure until otherwise determined
+ if (!$reply) {
+ failed("updating %s: failed to visit URL %s", $h, $url);
+ next;
+ }
+ next if !header_ok($h, $reply);
+ $reply =~ s/^.*?\n\n//s; # Strip the headers.
+ if ($reply !~ /Success/i) {
+ $reply =~ /^responseCode = (\d+)$/m;
+ my $code = $1 // '';
+ $reply =~ /^errors_0_message = '(.*)'$/m;
+ my $message = $1 // '';
+ failed("updating %s: error %d: %s", $code, $message);
+ next;
+ }
+ $config{$h}{'ip'} = $ip;
+ $config{$h}{'mtime'} = $now;
+ $config{$h}{'status'} = 'good';
+ success("updating %s: IP address set to %s", $h, $ip);
+ }
+}
+
+######################################################################
+## nic_gandi_examples
+## by Jimmy Thrasibule
+######################################################################
+sub nic_gandi_examples {
+ return <<"EoEXAMPLE";
+o 'gandi'
+
+The 'gandi' protocol is used by the LiveDNS service offered by gandi.net.
+Description of Gandi's LiveDNS API can be found at:
+
+ https://api.gandi.net/docs/livedns/
+
+Available configuration variables:
+ * password: The Gandi API key. If you don’t have one yet, you can generate
+ your production API key from the API Key Page (in the Security section).
+ Required.
+ * zone: The DNS zone to be updated. Required.
+ * ttl: The time-to-live value associated with the updated DNS record.
+ Optional; uses Gandi's default (3h) if unset.
+
+Example ${program}.conf file entries:
+ ## Single host update.
+ protocol=gandi, \\
+ zone=example.com, \\
+ password=my-gandi-api-key, \\
+ host.example.com
+
+ ## Multiple host update.
+ protocol=gandi, \\
+ zone=example.com, \\
+ password=my-gandi-api-key, \\
+ ttl=1h \\
+ hosta.example.com,hostb.sub.example.com
+EoEXAMPLE
+}
+
+######################################################################
+## nic_gandi_update
+######################################################################
+sub nic_gandi_update {
+ debug("\nnic_gandi_update -------------------");
+
+ # Update each set configured host.
+ foreach my $h (@_) {
+ my $ip = delete $config{$h}{'wantip'};
+ (my $hostname = $h) =~ s/\.\Q$config{$h}{zone}\E$//;
+
+ info("%s -- Setting IP address to %s.", $h, $ip);
+ verbose("UPDATE:", "updating %s", $h);
+
+ my $headers;
+ $headers = "Content-Type: application/json\n";
+ $headers .= "Authorization: Apikey $config{$h}{'password'}\n";
+
+ my $data = encode_json({
+ defined($config{$h}{'ttl'}) ? (rrset_ttl => $config{$h}{'ttl'}) : (),
+ rrset_values => [$ip],
+ });
+
+ my $rrset_type = is_ipv6($ip) ? "AAAA" : "A";
+ my $url;
+ $url = "https://$config{$h}{'server'}$config{$h}{'script'}";
+ $url .= "/livedns/domains/$config{$h}{'zone'}/records/$hostname/$rrset_type";
+
+ my $reply = geturl(
+ proxy => opt('proxy'),
+ url => $url,
+ headers => $headers,
+ method => 'PUT',
+ data => $data,
+ );
+ unless ($reply) {
+ failed("%s -- Could not connect to %s.", $h, $config{$h}{'server'});
+ next;
+ }
+ my $ok = header_ok($h, $reply);
+
+ $reply =~ s/^.*?\n\n//s;
+ my $response = eval { decode_json($reply) };
+ if (!defined($response)) {
+ $config{$h}{'status'} = "bad";
+
+ failed("%s -- Unexpected service response.", $h);
+ next;
+ }
+
+ if ($ok) {
+ $config{$h}{'ip'} = $ip;
+ $config{$h}{'mtime'} = $now;
+ $config{$h}{'status'} = "good";
+
+ success("%s -- Updated successfully to %s.", $h, $ip);
+ } else {
+ $config{$h}{'status'} = "bad";
+
+ if (defined($response->{status}) && $response->{status} eq "error") {
+ my @errors;
+ for my $err (@{$response->{errors}}) {
+ push(@errors, $err->{description});
+ }
+ failed("%s -- %s.", $h, join(", ", @errors));
+ } else {
+ failed("%s -- Unexpected service response.", $h);
+ }
+ }
+ }
+}
+######################################################################
+## nic_keysystems_examples
+######################################################################
+sub nic_keysystems_examples {
+ return <main() unless caller() && caller() ne 'PAR';
+
+######################################################################
+## Emacs and Vim settings
+
+# Local Variables:
+# mode: perl
+# fill-column: 99
+# indent-tabs-mode: nil
+# perl-indent-level: 4
+# tab-width: 8
+# End:
+
+# vim: ai et ts=8 sw=4 tw=99 cc=+1 filetype=perl
+
+__END__
diff -Nru ddclient-3.9.1/debian/changelog ddclient-3.10.0/debian/changelog
--- ddclient-3.9.1/debian/changelog 2022-10-15 09:59:04.000000000 +0000
+++ ddclient-3.10.0/debian/changelog 2023-01-16 02:47:24.000000000 +0000
@@ -1,3 +1,40 @@
+ddclient (3.10.0-2) unstable; urgency=medium
+
+ * Fix SSL certificate verification with IPv6 address literals
+ (Closes: #1028849)
+
+ -- Richard Hansen Sun, 15 Jan 2023 21:47:24 -0500
+
+ddclient (3.10.0-1) unstable; urgency=medium
+
+ [ Debian Janitor ]
+ * Remove constraints unnecessary since buster (oldstable):
+ + ddclient: Drop versioned constraint on lsb-base in Depends.
+
+ [ Richard Hansen ]
+ * Fix Portuguese (Portugal) debconf translation
+ * Update lintian override to accommodate newer lintian versions
+ * Bump debhelper-compat to 13 (no changes needed)
+ * Bump Standards-Version to 4.6.1 (no changes needed)
+ * Update debian/copyright
+ * New upstream version 3.10.0 (Closes: #1021345)
+ + Refresh patches
+ + Update dependencies
+ + Accommodate upstream's new build system
+ + Update debian/copyright for third-party modules in t/lib
+ + debconf: Update protocols and built-in IP discovery services
+ + Fix version in configure.ac
+ + Fix flawed default interface IP address tests
+ * Remove extra quotes in sysvinit script (Closes: #998650)
+ * postinst: Update dh_installinit and dh_installsystemd generated code
+ (Closes: #1022810)
+ * Update Portuguese (Portugal) debconf translation.
+ Thanks to Américo Monteiro (Closes: #982327)
+ * Remove unnecessary dependency on lsb-base
+ * prerm: Update dh_installinit and dh_installsystemd generated code
+
+ -- Richard Hansen Tue, 29 Nov 2022 13:30:24 -0500
+
ddclient (3.9.1-7.1) unstable; urgency=medium
* Non-maintainer upload.
diff -Nru ddclient-3.9.1/debian/config ddclient-3.10.0/debian/config
--- ddclient-3.9.1/debian/config 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/config 2023-01-16 02:47:24.000000000 +0000
@@ -182,9 +182,8 @@
db_get ddclient/web; web=${RET%% *}
case ${web} in
other) db_input critical ddclient/web-url || true;;
- # Use ddclient's built-in shorthands when possible
- dyndns|loopia) db_set ddclient/web-url "";;
- *) db_set ddclient/web-url "${RET##* }";;
+ # Use ddclient's built-in shorthands.
+ *) db_set ddclient/web-url "";;
esac
;;
"Network interface")
diff -Nru ddclient-3.9.1/debian/control ddclient-3.10.0/debian/control
--- ddclient-3.9.1/debian/control 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/control 2023-01-16 02:47:24.000000000 +0000
@@ -2,8 +2,21 @@
Section: net
Priority: optional
Maintainer: Richard Hansen
-Build-Depends: debhelper-compat (= 12), dh-exec, po-debconf, xmlto, quilt
-Standards-Version: 4.5.0
+Build-Depends: debhelper-compat (= 13), dh-exec, po-debconf, xmlto, quilt
+Build-Depends-Indep:
+ libhttp-daemon-perl (>= 6.12) ,
+ libhttp-daemon-ssl-perl ,
+ libhttp-message-perl ,
+ libio-socket-inet6-perl ,
+ libio-socket-ip-perl ,
+ libio-socket-ssl-perl ,
+ libplack-perl ,
+ libtest-mockmodule-perl ,
+ libtest-tcp-perl ,
+ libtest-warnings-perl ,
+ liburi-perl ,
+ perl
+Standards-Version: 4.6.1
Homepage: https://ddclient.net
Vcs-Git: https://salsa.debian.org/debian/ddclient.git
Vcs-Browser: https://salsa.debian.org/debian/ddclient
@@ -11,9 +24,20 @@
Package: ddclient
Architecture: all
-Pre-Depends: ${misc:Pre-Depends}
-Depends: ${perl:Depends}, libdata-validate-ip-perl, ${misc:Depends}, lsb-base (>= 3.1)
-Recommends: libio-socket-ssl-perl, libio-socket-inet6-perl, libdigest-sha-perl, libjson-pp-perl
+# init-system-helpers is normally included in ${misc:Pre-Depends}, but not when
+# --no-enable --no-start is passed to dh_installinit. It is included here to
+# silence a skip-systemd-native-flag-missing-pre-depends lintian warning. (It
+# is unclear whether this dependency is actually required, as
+# init-system-helpers is an essential package. dh_installinit uses a versioned
+# requirement for the init-system-helpers dependency so the same is done here.)
+Pre-Depends: ${misc:Pre-Depends}, init-system-helpers (>= 1.54~)
+Depends: ${perl:Depends}, ${misc:Depends}
+Recommends:
+ iproute2,
+ libio-socket-ssl-perl,
+ libio-socket-inet6-perl,
+ libdigest-sha-perl,
+ libjson-pp-perl,
Provides: dyndns-client
Description: address updating utility for dynamic DNS services
This package provides a client to update dynamic IP addresses with
diff -Nru ddclient-3.9.1/debian/copyright ddclient-3.10.0/debian/copyright
--- ddclient-3.9.1/debian/copyright 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/copyright 2023-01-16 02:47:24.000000000 +0000
@@ -10,7 +10,8 @@
Files: debian/*
Copyright:
- 2005-2014 Torsten Landschoff
+ 2020-2023 Richard Hansen
+ 2005-2016 Torsten Landschoff
2014 Tong Sun
2013 Teemu Ikonen
2013 Antonio Terceiro
@@ -24,6 +25,16 @@
Files: debian/po/*
Copyright:
+ 2021 Américo Monteiro
+ 2020 Ari Ervasti
+ 2020 Jean-Pierre Giraud
+ 2020 Adriano Rafael Gomes
+ 2020 José Vieira
+ 2020 Sebastian Rasmussen
+ 2020 Rui Branco
+ 2020 Takuma Yamada
+ 2020 Richard Hansen
+ 2014, 2020 Frans Spiesschaert
2014 Miroslav Kure
2014 Kenshi Muto
2014 Olexandr Kravchuk
@@ -41,7 +52,7 @@
2009 marce villarino
2009 Esko Arajärvi
2009 Bjørn Steensrud
- 2009, 2010 Yuri Kozlov
+ 2009, 2010, 2020 Yuri Kozlov
2009, 2010 Miguel Figueiredo
2009, 2010 Martin Bagge
2009, 2010 Ivan Masár
@@ -52,7 +63,7 @@
2007 Jacobo Tarrio
2006 Ricardo Silva
2006, 2009 Jordà Polo
- 2006, 2007, 2009 Helge Kreutzmann
+ 2006, 2007, 2009, 2020 Helge Kreutzmann
2005-2010 Clytie Siddall
2005-2009 Debian French l10n team
2005, 2009, 2010 Christian Perrier
@@ -60,6 +71,22 @@
2002 Felix Kröger
License: GPL-2.0+
+Files: t/lib/Devel/Autoflush.pm
+Copyright: 2014 David Golden
+License: Apache-2.0
+
+Files:
+ t/lib/Test/*
+ t/lib/Test2.pm
+ t/lib/Test2/*
+ t/lib/ok.pm
+Copyright: 2019 Chad Granum
+License: Artistic or GPL-1+
+
+Files: t/lib/ddclient/Test/Fake/HTTPD.pm
+Copyright: 2011-2017 NAKAGAWA Masaki
+License: Artistic or GPL-1+
+
License: GPL-2.0+
This package is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -76,3 +103,35 @@
.
On Debian systems, the complete text of the GNU General
Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
+
+License: Apache-2.0
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ .
+ http://www.apache.org/licenses/LICENSE-2.0
+ .
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ .
+ On Debian systems, the complete text of version 2.0 of the Apache
+ License can be found in "/usr/share/common-licenses/Apache-2.0".
+
+License: Artistic
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the Artistic License, which comes with Perl.
+ .
+ On Debian systems, the complete text of the Artistic License can be
+ found in "/usr/share/common-licenses/Artistic".
+
+License: GPL-1+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 1, or (at your option)
+ any later version.
+ .
+ On Debian systems, the complete text of version 1 of the GNU General
+ Public License can be found in "/usr/share/common-licenses/GPL-1".
diff -Nru ddclient-3.9.1/debian/ddclient.dhclient-exit-hook ddclient-3.10.0/debian/ddclient.dhclient-exit-hook
--- ddclient-3.9.1/debian/ddclient.dhclient-exit-hook 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/ddclient.dhclient-exit-hook 2023-01-16 02:47:24.000000000 +0000
@@ -3,7 +3,7 @@
# Run inside subshell so we can use exit to bail.
(
- [ -x /usr/sbin/ddclient ] || exit 0
+ [ -x /usr/bin/ddclient ] || exit 0
[ -f /etc/default/ddclient ] || exit 0
. /etc/default/ddclient
[ "$run_dhclient" = "true" ] || exit 0
@@ -11,7 +11,7 @@
case $reason in
BOUND|RENEW|REBIND)
/usr/bin/logger -t dhclient "$reason, updating IP address with ddclient"
- /usr/sbin/ddclient -daemon=0 -syslog > /dev/null 2>&1
+ /usr/bin/ddclient -daemon=0 -syslog > /dev/null 2>&1
;;
*)
;;
diff -Nru ddclient-3.9.1/debian/ddclient.init ddclient-3.10.0/debian/ddclient.init
--- ddclient-3.9.1/debian/ddclient.init 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/ddclient.init 2023-01-16 02:47:24.000000000 +0000
@@ -15,7 +15,7 @@
PATH=/sbin:/bin:/usr/sbin:/usr/bin
NAME="ddclient"
-DAEMON=/usr/sbin/$NAME
+DAEMON=/usr/bin/$NAME
PERL=/usr/bin/perl
DESC="Dynamic DNS service update utility"
@@ -53,7 +53,7 @@
start-stop-daemon --start \
--pidfile "$PIDFILE" --exec "$PERL" --startas "$DAEMON" \
- -- "$OPTIONS" \
+ -- $OPTIONS \
|| return 2
}
diff -Nru ddclient-3.9.1/debian/ddclient.install ddclient-3.10.0/debian/ddclient.install
--- ddclient-3.9.1/debian/ddclient.install 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/ddclient.install 2023-01-16 02:47:24.000000000 +0000
@@ -1,3 +1,2 @@
#!/usr/bin/dh-exec
-ddclient usr/sbin
debian/ddclient.dhclient-exit-hook => etc/dhcp/dhclient-exit-hooks.d/ddclient
diff -Nru ddclient-3.9.1/debian/ddclient.lintian-overrides ddclient-3.10.0/debian/ddclient.lintian-overrides
--- ddclient-3.9.1/debian/ddclient.lintian-overrides 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/ddclient.lintian-overrides 2023-01-16 02:47:24.000000000 +0000
@@ -1 +1,3 @@
-ddclient: duplicate-updaterc.d-calls-in-postinst ddclient
+# Lintian limitation: The two calls to update-rc.d are in two different branches
+# of an `if` statement so it's not actually called twice.
+ddclient: duplicate-updaterc.d-calls-in-postinst ddclient [postinst:*]
diff -Nru ddclient-3.9.1/debian/ddclient.ppp.ip-up ddclient-3.10.0/debian/ddclient.ppp.ip-up
--- ddclient-3.9.1/debian/ddclient.ppp.ip-up 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/ddclient.ppp.ip-up 2023-01-16 02:47:24.000000000 +0000
@@ -12,7 +12,7 @@
# only run ddclient, if it is installed ;-)
-if [ ! -x /usr/sbin/ddclient ]; then
+if [ ! -x /usr/bin/ddclient ]; then
exit 0
fi
@@ -34,4 +34,4 @@
# Run ddclient with the IP address of the ppp device
-/usr/sbin/ddclient -syslog -ip $PPP_LOCAL
+/usr/bin/ddclient -syslog -ip $PPP_LOCAL
diff -Nru ddclient-3.9.1/debian/ddclient.service ddclient-3.10.0/debian/ddclient.service
--- ddclient-3.9.1/debian/ddclient.service 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/ddclient.service 2023-01-16 02:47:24.000000000 +0000
@@ -9,7 +9,7 @@
PIDFile=/run/ddclient.pid
Environment=daemon_interval=5m
EnvironmentFile=-/etc/default/ddclient
-ExecStart=/usr/sbin/ddclient -daemon $daemon_interval -syslog -pid /run/ddclient.pid
+ExecStart=/usr/bin/ddclient -daemon $daemon_interval -syslog -pid /run/ddclient.pid
Restart=on-failure
[Install]
diff -Nru ddclient-3.9.1/debian/ddclient.templates ddclient-3.10.0/debian/ddclient.templates
--- ddclient-3.9.1/debian/ddclient.templates 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/ddclient.templates 2023-01-16 02:47:24.000000000 +0000
@@ -25,8 +25,8 @@
Template: ddclient/protocol
Type: select
-#flag:translate:25
-__Choices: changeip, cloudflare, concont, dnsmadeeasy, dnspark, dondominio, dslreports1, dtdns, duckdns, dyndns1, dyndns2, easydns, freedns, freemyip, googledomains, hammernode1, namecheap, nfsn, noip, nsupdate, sitelutions, woima, yandex, zoneedit1, other
+#flag:translate:31
+__Choices: 1984, changeip, cloudflare, cloudns, dinahosting, dnsexit, dnsmadeeasy, dondominio, dslreports1, duckdns, dyndns1, dyndns2, easydns, freedns, freemyip, gandi, godaddy, googledomains, hetzner, keysystems, namecheap, nfsn, njalla, noip, nsupdate, ovh, sitelutions, woima, yandex, zoneedit1, other
Default: dyndns2
_Description: Dynamic DNS update protocol:
Dynamic DNS update protocol used by your dynamic DNS service
diff -Nru ddclient-3.9.1/debian/dirs ddclient-3.10.0/debian/dirs
--- ddclient-3.9.1/debian/dirs 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/dirs 2023-01-16 02:47:24.000000000 +0000
@@ -1,4 +1,3 @@
-usr/sbin
var/cache/ddclient
etc/ppp/ip-up.d
etc/dhcp/dhclient-exit-hooks.d
diff -Nru ddclient-3.9.1/debian/examples ddclient-3.10.0/debian/examples
--- ddclient-3.9.1/debian/examples 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/examples 2023-01-16 02:47:24.000000000 +0000
@@ -1,5 +1,4 @@
sample-ddclient-wrapper.sh
sample-etc_cron.d_ddclient
-sample-etc_ddclient.conf
sample-etc_dhcpc_dhcpcd-eth0.exe
sample-get-ip-from-fritzbox
diff -Nru ddclient-3.9.1/debian/patches/boolean-parse.patch ddclient-3.10.0/debian/patches/boolean-parse.patch
--- ddclient-3.9.1/debian/patches/boolean-parse.patch 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/patches/boolean-parse.patch 1970-01-01 00:00:00.000000000 +0000
@@ -1,29 +0,0 @@
-From: Richard Hansen
-Date: Tue, 16 Jun 2020 22:58:09 -0400
-Subject: Fix parsing of "true" as a boolean value
-
-Before, "t" and "ttrue" were accepted as true, but not "true".
-
-Also simplify the true and false regular expressions.
-
-Origin: upstream, https://github.com/ddclient/ddclient/commit/40f355d05ef8f0a194f100784216fe1e928e8394
----
- ddclient | 4 ++--
- 1 file changed, 2 insertions(+), 2 deletions(-)
-
-diff --git a/ddclient b/ddclient
-index 5e4876d..655b0aa 100755
---- a/ddclient
-+++ b/ddclient
-@@ -1897,9 +1897,9 @@ sub check_value {
- $value = $min if defined($min) && $value < $min;
-
- } elsif ($type eq T_BOOL) {
-- if ($value =~ /^y(es)?$|^t(true)?$|^1$/i) {
-+ if ($value =~ /^(y(es)?|t(rue)?|1)$/i) {
- $value = 1;
-- } elsif ($value =~ /^n(o)?$|^f(alse)?$|^0$/i) {
-+ } elsif ($value =~ /^(n(o)?|f(alse)?|0)$/i) {
- $value = 0;
- } else {
- return undef;
diff -Nru ddclient-3.9.1/debian/patches/config_path.diff ddclient-3.10.0/debian/patches/config_path.diff
--- ddclient-3.9.1/debian/patches/config_path.diff 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/patches/config_path.diff 1970-01-01 00:00:00.000000000 +0000
@@ -1,22 +0,0 @@
-From: Torsten Landschoff
-Date: Wed, 5 Apr 2006 02:01:22 +0200
-Subject: From version 3.6.7-1, config_path.diff
-
-Forwarded: no
----
- ddclient | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/ddclient b/ddclient
-index 4a2d06c..f2acbfc 100755
---- a/ddclient
-+++ b/ddclient
-@@ -33,7 +33,7 @@ my $program = $programd;
- $program =~ s/d$//;
- my $now = time;
- my $hostname = hostname();
--my $etc = ($program =~ /test/i) ? './' : '/etc/ddclient/';
-+my $etc = ($program =~ /test/i) ? './' : '/etc/';
- my $cachedir = ($program =~ /test/i) ? './' : '/var/cache/ddclient/';
- my $savedir = ($program =~ /test/i) ? 'URL/' : '/tmp/';
- my $msgs = '';
diff -Nru ddclient-3.9.1/debian/patches/devices.patch ddclient-3.10.0/debian/patches/devices.patch
--- ddclient-3.9.1/debian/patches/devices.patch 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/patches/devices.patch 1970-01-01 00:00:00.000000000 +0000
@@ -1,55 +0,0 @@
-From: Geoff Simmons
-Date: Fri, 23 Jul 2010 03:11:17 +1000
-Subject: Add new device built-ins (`fw=`)
-
-Forwarded: https://github.com/ddclient/ddclient/pull/263
-Applied-Upstream: https://github.com/ddclient/ddclient/commit/f8185182e97d7fda6c920e2c4ec94c3f3e7d69f0
----
- ddclient | 20 ++++++++++++++++++++
- 1 file changed, 20 insertions(+)
-
-diff --git a/ddclient b/ddclient
-index 984fc4f..82b3d10 100755
---- a/ddclient
-+++ b/ddclient
-@@ -178,6 +178,11 @@ my %builtinfw = (
- 'url' => '/cgi/ip/',
- 'skip' => 'ppp',
- },
-+ 'alcatel-530' => {
-+ 'name' => 'Alcatel/Thomson SpeedTouch 530',
-+ 'url' => '/cgi/status/',
-+ 'skip' => 'IP Address',
-+ },
- 'allnet-1298' => {
- 'name' => 'Allnet 1298',
- 'url' => '/cgi/router/',
-@@ -198,6 +203,16 @@ my %builtinfw = (
- 'url' => '/shell/show+ip+interfaces',
- 'skip' => '.*inet',
- },
-+ 'thomson-st536v6' => {
-+ 'name' => 'Thomson SpeedTouch 536v6',
-+ 'url' => '/cgi/b/is/',
-+ 'skip' => 'IP Address',
-+ },
-+ 'thomson-tg782' => {
-+ 'name' => 'Thomson/Technicolor TG782',
-+ 'url' => '/cgi/b/is/',
-+ 'skip' => 'IP Address',
-+ },
- 'vigor-2200usb' => {
- 'name' => 'Vigor 2200 USB',
- 'url' => '/doc/online.sht',
-@@ -288,6 +303,11 @@ my %builtinfw = (
- 'url' => '/status_main.stm',
- 'skip' => 'var wan_ip=',
- },
-+ 'siemens-ss4200' => {
-+ 'name' => 'Siemens SpeedStream 4200',
-+ 'url' => '/summary.htm',
-+ 'skip' => '',
-+ },
- 'sitecom-dc202' => {
- 'name' => 'Sitecom DC-202 FW',
- 'url' => '/status.htm',
diff -Nru ddclient-3.9.1/debian/patches/dslreports.patch ddclient-3.10.0/debian/patches/dslreports.patch
--- ddclient-3.9.1/debian/patches/dslreports.patch 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/patches/dslreports.patch 1970-01-01 00:00:00.000000000 +0000
@@ -1,33 +0,0 @@
-From: Richard Hansen
-Date: Sat, 1 Aug 2020 16:22:38 -0400
-Subject: Change the default server for `dslreports1` to `www.dslreports.com`
-
-Before, it defaulted to `members.dyndns.org` which didn't make much
-sense.
-
-Forwarded: https://github.com/ddclient/ddclient/pull/264
-Applied-Upstream: https://github.com/ddclient/ddclient/commit/25a636879f922234cb7ec8f1aa945f74d7cd2a38
----
- ddclient | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/ddclient b/ddclient
-index 71e69be..cfda556 100755
---- a/ddclient
-+++ b/ddclient
-@@ -569,6 +569,7 @@ my %services = (
- 'update' => \&nic_dslreports1_update,
- 'examples' => \&nic_dslreports1_examples,
- 'variables' => merge(
-+ { 'server' => setv(T_FQDNP, 1, 0, 1, 'www.dslreports.com', undef) },
- { 'host' => setv(T_NUMBER, 1, 1, 1, 0, undef) },
- $variables{'service-common-defaults'},
- ),
-@@ -3195,7 +3196,6 @@ Configuration variables applicable to the 'dslreports1' protocol are:
- Example ${program}.conf file entries:
- ## single host update
- protocol=dslreports1, \\
-- server=www.dslreports.com, \\
- login=my-dslreports-login, \\
- password=my-dslreports-password \\
- 123456
diff -Nru ddclient-3.9.1/debian/patches/fix-default-interface-tests.diff ddclient-3.10.0/debian/patches/fix-default-interface-tests.diff
--- ddclient-3.9.1/debian/patches/fix-default-interface-tests.diff 1970-01-01 00:00:00.000000000 +0000
+++ ddclient-3.10.0/debian/patches/fix-default-interface-tests.diff 2023-01-16 02:47:24.000000000 +0000
@@ -0,0 +1,65 @@
+From: Richard Hansen
+Date: Wed, 23 Nov 2022 02:11:16 -0500
+Subject: Fix flawed default interface IP address tests
+
+`ddclient::get_default_interface()` doesn't appear to return anything
+on the salsa.debian.org CI runners, and subtests can't have 0
+checks.
+
+Also, some systems don't have IPv4 or IPv6 addresses on the default
+interface.
+
+Forwarded: no
+---
+ t/get_ip_from_if.pl | 37 +++++++++++++++++++------------------
+ 1 file changed, 19 insertions(+), 18 deletions(-)
+
+diff --git a/t/get_ip_from_if.pl b/t/get_ip_from_if.pl
+index 6f08e5d..cc0cabf 100644
+--- a/t/get_ip_from_if.pl
++++ b/t/get_ip_from_if.pl
+@@ -39,25 +39,26 @@ subtest "get_ip_from_interface tests" => sub {
+ }
+ };
+
+-subtest "Get default interface and IP for test system" => sub {
++subtest "Get default interface and IP for test system (IPv4)" => sub {
+ my $interface = ddclient::get_default_interface(4);
+- if ($interface) {
+- isnt($interface, "lo", "Check for loopback 'lo'");
+- isnt($interface, "lo0", "Check for loopback 'lo0'");
+- my $ip1 = ddclient::get_ip_from_interface("default", 4);
+- my $ip2 = ddclient::get_ip_from_interface($interface, 4);
+- is($ip1, $ip2, "Check IPv4 from default interface");
+- ok(ddclient::is_ipv4($ip1), "Valid IPv4 from get_ip_from_interface($interface)");
+- }
+- $interface = ddclient::get_default_interface(6);
+- if ($interface) {
+- isnt($interface, "lo", "Check for loopback 'lo'");
+- isnt($interface, "lo0", "Check for loopback 'lo0'");
+- my $ip1 = ddclient::get_ip_from_interface("default", 6);
+- my $ip2 = ddclient::get_ip_from_interface($interface, 6);
+- is($ip1, $ip2, "Check IPv6 from default interface");
+- ok(ddclient::is_ipv6($ip1), "Valid IPv6 from get_ip_from_interface($interface)");
+- }
++ plan skip_all => 'no IPv4 interface' if !$interface;
++ isnt($interface, "lo", "Check for loopback 'lo'");
++ isnt($interface, "lo0", "Check for loopback 'lo0'");
++ my $ip1 = ddclient::get_ip_from_interface("default", 4);
++ my $ip2 = ddclient::get_ip_from_interface($interface, 4);
++ is($ip1, $ip2, "Check IPv4 from default interface");
++ ok(!$ip1 || ddclient::is_ipv4($ip1), "Valid IPv4 from get_ip_from_interface($interface)");
++};
++
++subtest "Get default interface and IP for test system (IPv6)" => sub {
++ my $interface = ddclient::get_default_interface(6);
++ plan skip_all => 'no IPv6 interface' if !$interface;
++ isnt($interface, "lo", "Check for loopback 'lo'");
++ isnt($interface, "lo0", "Check for loopback 'lo0'");
++ my $ip1 = ddclient::get_ip_from_interface("default", 6);
++ my $ip2 = ddclient::get_ip_from_interface($interface, 6);
++ is($ip1, $ip2, "Check IPv6 from default interface");
++ ok(!$ip1 || ddclient::is_ipv6($ip1), "Valid IPv6 from get_ip_from_interface($interface)");
+ };
+
+ done_testing();
diff -Nru ddclient-3.9.1/debian/patches/fix-version.diff ddclient-3.10.0/debian/patches/fix-version.diff
--- ddclient-3.9.1/debian/patches/fix-version.diff 1970-01-01 00:00:00.000000000 +0000
+++ ddclient-3.10.0/debian/patches/fix-version.diff 2023-01-16 02:47:24.000000000 +0000
@@ -0,0 +1,20 @@
+From: Richard Hansen
+Date: Wed, 23 Nov 2022 00:57:53 -0500
+Subject: Fix version in configure.ac
+
+Forwarded: not-needed
+---
+ configure.ac | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/configure.ac b/configure.ac
+index 554ea6b..c825436 100644
+--- a/configure.ac
++++ b/configure.ac
+@@ -1,5 +1,5 @@
+ AC_PREREQ([2.63])
+-AC_INIT([ddclient], [3.10.0_2])
++AC_INIT([ddclient], [3.10.0])
+ AC_CONFIG_SRCDIR([ddclient.in])
+ AC_CONFIG_AUX_DIR([build-aux])
+ AC_CONFIG_MACRO_DIR([m4])
diff -Nru ddclient-3.9.1/debian/patches/maxinterval.diff ddclient-3.10.0/debian/patches/maxinterval.diff
--- ddclient-3.9.1/debian/patches/maxinterval.diff 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/patches/maxinterval.diff 2023-01-16 02:47:24.000000000 +0000
@@ -4,19 +4,19 @@
Forwarded: no
---
- ddclient | 2 +-
+ ddclient.in | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
-diff --git a/ddclient b/ddclient
-index f2acbfc..3b40f57 100755
---- a/ddclient
-+++ b/ddclient
-@@ -393,7 +393,7 @@ my %variables = (
- 'atime' => setv(T_NUMBER, 0, 1, 0, 0, undef),
- 'status' => setv(T_ANY, 0, 1, 0, '', undef),
- 'min-interval' => setv(T_DELAY, 0, 0, 1, interval('30s'), 0),
-- 'max-interval' => setv(T_DELAY, 0, 0, 1, interval('25d'), 0),
-+ 'max-interval' => setv(T_DELAY, 0, 0, 1, interval('30d'), 0),
- 'min-error-interval' => setv(T_DELAY, 0, 0, 1, interval('5m'), 0),
+diff --git a/ddclient.in b/ddclient.in
+index 6687f8b..bdd4bd9 100755
+--- a/ddclient.in
++++ b/ddclient.in
+@@ -511,7 +511,7 @@ my %variables = (
+ 'status-ipv4' => setv(T_ANY, 0, 1, '', undef),
+ 'status-ipv6' => setv(T_ANY, 0, 1, '', undef),
+ 'min-interval' => setv(T_DELAY, 0, 0, interval('30s'), 0),
+- 'max-interval' => setv(T_DELAY, 0, 0, interval('25d'), 0),
++ 'max-interval' => setv(T_DELAY, 0, 0, interval('30d'), 0),
+ 'min-error-interval' => setv(T_DELAY, 0, 0, interval('5m'), 0),
- 'warned-min-interval' => setv(T_ANY, 0, 1, 0, 0, undef),
+ 'warned-min-interval' => setv(T_ANY, 0, 1, 0, undef),
diff -Nru ddclient-3.9.1/debian/patches/news.diff ddclient-3.10.0/debian/patches/news.diff
--- ddclient-3.9.1/debian/patches/news.diff 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/patches/news.diff 2023-01-16 02:47:24.000000000 +0000
@@ -1,6 +1,6 @@
From: Richard Hansen
Date: Sat, 23 May 2020 16:21:16 -0400
-Subject: Rename RELEASENOTE to NEWS
+Subject: Rename ChangeLog.md to NEWS
This ensures that the file is installed as NEWS.gz as required in
Debian policy >= 4.2.0. For details, see:
@@ -8,11 +8,11 @@
Forwarded: no
---
- RELEASENOTE => NEWS | 0
+ ChangeLog.md => NEWS | 0
1 file changed, 0 insertions(+), 0 deletions(-)
- rename RELEASENOTE => NEWS (100%)
+ rename ChangeLog.md => NEWS (100%)
-diff --git a/RELEASENOTE b/NEWS
+diff --git a/ChangeLog.md b/NEWS
similarity index 100%
-rename from RELEASENOTE
+rename from ChangeLog.md
rename to NEWS
diff -Nru ddclient-3.9.1/debian/patches/series ddclient-3.10.0/debian/patches/series
--- ddclient-3.9.1/debian/patches/series 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/patches/series 2023-01-16 02:47:24.000000000 +0000
@@ -1,10 +1,5 @@
-shebang.diff
news.diff
-smc-barricade-fw-alt.diff
-config_path.diff
maxinterval.diff
-usev6.diff
-boolean-parse.patch
-use-ipv4.patch
-devices.patch
-dslreports.patch
+fix-version.diff
+fix-default-interface-tests.diff
+ssl-hostname-verification.patch
diff -Nru ddclient-3.9.1/debian/patches/shebang.diff ddclient-3.10.0/debian/patches/shebang.diff
--- ddclient-3.9.1/debian/patches/shebang.diff 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/patches/shebang.diff 1970-01-01 00:00:00.000000000 +0000
@@ -1,30 +0,0 @@
-From: Richard Hansen
-Date: Sat, 23 May 2020 14:33:22 -0400
-Subject: Set shebang as required by Debian policy >= 4.1.2
-
-See https://www.debian.org/doc/debian-policy/ch-files.html#scripts
-
-Forwarded: https://github.com/ddclient/ddclient/pull/116
----
- ddclient | 4 ++--
- 1 file changed, 2 insertions(+), 2 deletions(-)
-
-diff --git a/ddclient b/ddclient
-index 8a1bacc..144cdd3 100755
---- a/ddclient
-+++ b/ddclient
-@@ -1,5 +1,4 @@
--#!/usr/bin/perl -w
--#!/usr/local/bin/perl -w
-+#!/usr/bin/perl
- ######################################################################
- #
- # DDCLIENT - a Perl client for updating DynDNS information
-@@ -21,6 +20,7 @@
- ######################################################################
- require 5.004;
- use strict;
-+use warnings;
- use Getopt::Long;
- use Sys::Hostname;
- use IO::Socket;
diff -Nru ddclient-3.9.1/debian/patches/smc-barricade-fw-alt.diff ddclient-3.10.0/debian/patches/smc-barricade-fw-alt.diff
--- ddclient-3.9.1/debian/patches/smc-barricade-fw-alt.diff 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/patches/smc-barricade-fw-alt.diff 1970-01-01 00:00:00.000000000 +0000
@@ -1,25 +0,0 @@
-From: Torsten Landschoff
-Date: Fri, 10 Aug 2012 02:10:02 +0200
-Subject: From version 3.6.7-1, smc-barricade-fw-alt.diff
-
-Forwarded: no
----
- ddclient | 5 +++++
- 1 file changed, 5 insertions(+)
-
-diff --git a/ddclient b/ddclient
-index 144cdd3..4a2d06c 100755
---- a/ddclient
-+++ b/ddclient
-@@ -92,6 +92,11 @@ my %builtinfw = (
- 'url' => '/status.HTM',
- 'skip' => 'WAN IP',
- },
-+ 'smc-barricade-alt' => {
-+ 'name' => 'SMC Barricade FW (alternate config)',
-+ 'url' => '/status.HTM',
-+ 'skip' => 'WAN IP',
-+ },
- 'smc-barricade-7401bra' => {
- 'name' => 'SMC Barricade 7401BRA FW',
- 'url' => '/admin/wan1.htm',
diff -Nru ddclient-3.9.1/debian/patches/ssl-hostname-verification.patch ddclient-3.10.0/debian/patches/ssl-hostname-verification.patch
--- ddclient-3.9.1/debian/patches/ssl-hostname-verification.patch 1970-01-01 00:00:00.000000000 +0000
+++ ddclient-3.10.0/debian/patches/ssl-hostname-verification.patch 2023-01-16 02:47:24.000000000 +0000
@@ -0,0 +1,156 @@
+From: Richard Hansen
+Date: Sun, 15 Jan 2023 20:15:07 -0500
+Subject: Set `SSL_verifycn_scheme` and `SSL_verifycn_name` on SSL connection
+
+IO::Socket::SSL recently changed hostname verification of literal IP
+addresses; see .
+Literal IP addresses are unlikely to be used by users, but they are
+used in unit tests, so the tests started failing. To fix the tests,
+and to fix any rare non-test usages, unconditionally set
+`SSL_verify_name` to the peer hostname or IP address literal.
+
+While we're here, set `SSL_verifycn_scheme` to `"http"` as encouraged
+by the IO::Socket::SSL documentation.
+
+Bug: https://github.com/noxxi/p5-io-socket-ssl/issues/123
+Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1028849
+Forwarded: no
+---
+ ddclient.in | 2 ++
+ t/geturl_ssl.pl | 26 ++++++++++++++++++++++++++
+ 2 files changed, 28 insertions(+)
+
+diff --git a/ddclient.in b/ddclient.in
+index 28e2330..13a1a05 100755
+--- a/ddclient.in
++++ b/ddclient.in
+@@ -2497,6 +2497,8 @@ sub fetch_via_socket_io {
+ $socket_args{SSL_verify_mode} = ($params{ssl_validate} // 1)
+ ? IO::Socket::SSL->SSL_VERIFY_PEER
+ : IO::Socket::SSL->SSL_VERIFY_NONE;
++ $socket_args{SSL_verifycn_scheme} = 'http';
++ $socket_args{SSL_verifycn_name} = $peer;
+ } elsif ($globals{'ipv6'} || $ipversion eq '6') {
+ load_ipv6_support;
+ $socket_class = 'IO::Socket::INET6';
+diff --git a/t/geturl_ssl.pl b/t/geturl_ssl.pl
+index c070def..d034c4b 100644
+--- a/t/geturl_ssl.pl
++++ b/t/geturl_ssl.pl
+@@ -59,6 +59,8 @@ my @test_cases = (
+ PeerAddr => 'hostname',
+ PeerPort => '443',
+ SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
++ SSL_verifycn_scheme => 'http',
++ SSL_verifycn_name => 'hostname',
+ },
+ want_req_uri => '/',
+ },
+@@ -72,6 +74,8 @@ my @test_cases = (
+ PeerAddr => 'hostname',
+ PeerPort => '443',
+ SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
++ SSL_verifycn_scheme => 'http',
++ SSL_verifycn_name => 'hostname',
+ },
+ want_req_uri => '/',
+ },
+@@ -84,6 +88,8 @@ my @test_cases = (
+ PeerAddr => 'hostname',
+ PeerPort => '123',
+ SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
++ SSL_verifycn_scheme => 'http',
++ SSL_verifycn_name => 'hostname',
+ },
+ want_req_uri => '/',
+ },
+@@ -97,6 +103,8 @@ my @test_cases = (
+ PeerAddr => 'hostname',
+ PeerPort => '123',
+ SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
++ SSL_verifycn_scheme => 'http',
++ SSL_verifycn_name => 'hostname',
+ },
+ want_req_uri => '/',
+ },
+@@ -110,6 +118,8 @@ my @test_cases = (
+ PeerAddr => 'proxy',
+ PeerPort => '443',
+ SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
++ SSL_verifycn_scheme => 'http',
++ SSL_verifycn_name => 'proxy',
+ },
+ want_req_uri => 'http://hostname/',
+ todo => "broken",
+@@ -124,6 +134,8 @@ my @test_cases = (
+ PeerAddr => 'proxy',
+ PeerPort => '80',
+ SSL_startHandshake => 0,
++ SSL_verifycn_scheme => 'http',
++ SSL_verifycn_name => 'proxy',
+ },
+ want_req_method => 'CONNECT',
+ want_req_uri => 'hostname:443',
+@@ -139,6 +151,8 @@ my @test_cases = (
+ PeerAddr => 'proxy',
+ PeerPort => '443',
+ SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
++ SSL_verifycn_scheme => 'http',
++ SSL_verifycn_name => 'proxy',
+ },
+ want_req_method => 'CONNECT',
+ want_req_uri => 'hostname:443',
+@@ -155,6 +169,8 @@ my @test_cases = (
+ PeerAddr => 'proxy',
+ PeerPort => '443',
+ SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
++ SSL_verifycn_scheme => 'http',
++ SSL_verifycn_name => 'proxy',
+ },
+ want_req_method => 'CONNECT',
+ want_req_uri => 'hostname:443',
+@@ -169,6 +185,8 @@ my @test_cases = (
+ want_args => {
+ PeerAddr => 'proxy',
+ PeerPort => '123',
++ SSL_verifycn_scheme => 'http',
++ SSL_verifycn_name => 'proxy',
+ },
+ want_req_uri => 'http://hostname:456/',
+ todo => "broken",
+@@ -183,6 +201,8 @@ my @test_cases = (
+ PeerAddr => 'proxy',
+ PeerPort => '123',
+ SSL_startHandshake => 0,
++ SSL_verifycn_scheme => 'http',
++ SSL_verifycn_name => 'proxy',
+ },
+ want_req_method => 'CONNECT',
+ want_req_uri => 'hostname:456',
+@@ -199,6 +219,8 @@ my @test_cases = (
+ PeerPort => '443',
+ SSL_ca_path => '/ca/dir',
+ SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
++ SSL_verifycn_scheme => 'http',
++ SSL_verifycn_name => 'hostname',
+ },
+ want_req_uri => '/',
+ },
+@@ -213,6 +235,8 @@ my @test_cases = (
+ PeerPort => '443',
+ SSL_ca_file => '/ca/file',
+ SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
++ SSL_verifycn_scheme => 'http',
++ SSL_verifycn_name => 'hostname',
+ },
+ want_req_uri => '/',
+ },
+@@ -229,6 +253,8 @@ my @test_cases = (
+ SSL_ca_file => '/ca/file',
+ SSL_ca_path => '/ca/dir',
+ SSL_verify_mode => IO::Socket::SSL->SSL_VERIFY_PEER,
++ SSL_verifycn_scheme => 'http',
++ SSL_verifycn_name => 'hostname',
+ },
+ want_req_uri => '/',
+ },
diff -Nru ddclient-3.9.1/debian/patches/use-ipv4.patch ddclient-3.10.0/debian/patches/use-ipv4.patch
--- ddclient-3.9.1/debian/patches/use-ipv4.patch 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/patches/use-ipv4.patch 1970-01-01 00:00:00.000000000 +0000
@@ -1,29 +0,0 @@
-From: Richard Hansen
-Date: Tue, 16 Jun 2020 22:59:51 -0400
-Subject: Fix `use=ip` when `ip` is set to an IPv4 address
-
-Before, with `use=ip,ip=1.2.3.4`, `get_ip` would return `undef` and
-print a warning:
-
- WARNING: found neither ipv4 nor ipv6 address
-
-Origin: upstream, https://github.com/ddclient/ddclient/commit/99a60995c47348ea3281cf992f1605712b1c7598
----
- ddclient | 4 +++-
- 1 file changed, 3 insertions(+), 1 deletion(-)
-
-diff --git a/ddclient b/ddclient
-index 655b0aa..984fc4f 100755
---- a/ddclient
-+++ b/ddclient
-@@ -2326,7 +2326,9 @@ sub get_ip {
- $skip =~ s/ /\\s/is;
- $reply =~ s/^.*?${skip}//is;
- }
-- if ($reply =~ /^.*?\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b.*/is) {
-+ if (defined($ip)) {
-+ # no need to parse $reply
-+ } elsif ($reply =~ /^.*?\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b.*/is) {
- $ip = $1;
- $ip = un_zero_pad($ip);
- $ip = filter_local($ip) if opt('fw-banlocal', $h);
diff -Nru ddclient-3.9.1/debian/patches/usev6.diff ddclient-3.10.0/debian/patches/usev6.diff
--- ddclient-3.9.1/debian/patches/usev6.diff 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/patches/usev6.diff 1970-01-01 00:00:00.000000000 +0000
@@ -1,158 +0,0 @@
-From: Torsten Landschoff
-Date: Sun, 18 Jan 2015 20:54:02 +0100
-Subject: Add ipv6 support for dyndns
-MIME-Version: 1.0
-Content-Type: text/plain; charset="utf-8"
-Content-Transfer-Encoding: 8bit
-
-This adds a "usev6" variable to ddclient to support IPv6 updates
-with dyndns and freedns.
-
-Author: Eduardo Trápani
-Forwarded: no
-Bug-Debian: https://bugs.debian.org/704467
----
- ddclient | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
- 1 file changed, 61 insertions(+), 8 deletions(-)
-
-diff --git a/ddclient b/ddclient
-index 3b40f57..5e4876d 100755
---- a/ddclient
-+++ b/ddclient
-@@ -59,6 +59,7 @@ sub T_FILE {'file name'};
- sub T_FQDNP {'fully qualified host name and optional port number'};
- sub T_PROTO {'protocol'}
- sub T_USE {'ip strategy'}
-+sub T_USEV6 {'ipv6 strategy'}
- sub T_IF {'interface'}
- sub T_PROG {'program name'}
- sub T_IP {'ip'}
-@@ -334,6 +335,7 @@ my %variables = (
- 'protocol' => setv(T_PROTO, 0, 0, 1, 'dyndns2', undef),
-
- 'use' => setv(T_USE, 0, 0, 1, 'ip', undef),
-+ 'usev6' => setv(T_USEV6, 0, 0, 1, undef, undef),
- 'ip' => setv(T_IP, 0, 0, 1, undef, undef),
- 'if' => setv(T_IF, 0, 0, 1, 'ppp0', undef),
- 'if-skip' => setv(T_STRING,1, 0, 1, '', undef),
-@@ -375,6 +377,7 @@ my %variables = (
- 'host' => setv(T_STRING, 1, 1, 1, '', undef),
-
- 'use' => setv(T_USE, 0, 0, 1, 'ip', undef),
-+ 'usev6' => setv(T_USE, 0, 0, 1, undef, undef),
- 'if' => setv(T_IF, 0, 0, 1, 'ppp0', undef),
- 'if-skip' => setv(T_STRING,0, 0, 1, '', undef),
- 'web' => setv(T_STRING,0, 0, 1, 'dyndns', undef),
-@@ -759,7 +762,8 @@ my @opt = (
- [ "cache", "=s", "-cache path : record address used in 'path'" ],
- [ "pid", "=s", "-pid path : record process id in 'path'" ],
- "",
-- [ "use", "=s", "-use which : how the should IP address be obtained." ],
-+ [ "use", "=s", "-use which : how should the IP address be obtained." ],
-+ [ "usev6", "=s", "-usev6 which : how should the IPv6 address be obtained." ],
- &ip_strategies_usage(),
- "",
- [ "ip", "=s", "-ip address : set the IP address to 'address'" ],
-@@ -957,7 +961,8 @@ sub update_nics {
- next if $config{$h}{'protocol'} ne lc($s);
- $examined{$h} = 1;
- # we only do this once per 'use' and argument combination
-- my $use = opt('use', $h);
-+ my $ipv6 = defined($config{$h}{'usev6'});
-+ my $use = $ipv6 ? opt('usev6', $h) : opt('use', $h);
- my $arg_ip = opt('ip', $h) || '';
- my $arg_fw = opt('fw', $h) || '';
- my $arg_if = opt('if', $h) || '';
-@@ -967,17 +972,20 @@ sub update_nics {
- if (exists $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd}) {
- $ip = $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd};
- } else {
-- $ip = get_ip($use, $h);
-+ $ip = get_ip($use, $h) if !$ipv6;
-+ $ip = get_ipv6($use, $h) if $ipv6;
- if (!defined $ip || !$ip) {
- warning("unable to determine IP address")
- if !$daemon || opt('verbose');
- next;
- }
-- if ($ip !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
-- if( !ipv6_match($ip) ) {
-- warning("malformed IP address (%s)", $ip);
-- next;
-- }
-+ if (!$ipv6 and $ip !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
-+ warning("malformed IPv4 address (%s)", $ip);
-+ next;
-+ }
-+ if ($ipv6 and !ipv6_match($ip)) {
-+ warning("malformed IPv6 address (%s)", $ip);
-+ next;
- }
- $iplist{$use}{$arg_ip}{$arg_fw}{$arg_if}{$arg_web}{$arg_cmd} = $ip;
- }
-@@ -1297,6 +1305,11 @@ sub init_config {
- $opt{'use'} = 'if' if !define($opt{'use'}) && defined($opt{'if'});
- $opt{'use'} = 'web' if !define($opt{'use'}) && defined($opt{'web'});
-
-+ ## infer the IPv6 strategy if possible
-+ $opt{'usev6'} = 'ip' if !define($opt{'usev6'}) && defined($opt{'ip'});
-+ $opt{'usev6'} = 'if' if !define($opt{'usev6'}) && defined($opt{'if'});
-+ $opt{'usev6'} = 'web' if !define($opt{'usev6'}) && defined($opt{'web'});
-+
- ## 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')));
-@@ -1907,6 +1920,10 @@ sub check_value {
- $value = lc $value;
- return undef if ! exists $ip_strategies{$value};
-
-+ } elsif ($type eq T_USEV6) {
-+ $value = lc $value;
-+ return undef if ! exists $ip_strategies{$value};
-+
- } elsif ($type eq T_FILE) {
- return undef if $value eq "";
-
-@@ -2327,6 +2344,42 @@ sub get_ip {
- return $ip;
- }
-
-+######################################################################
-+## get_ipv6
-+######################################################################
-+sub get_ipv6 {
-+ my $use = lc shift;
-+ my $h = shift;
-+ my ($ip, $arg, $reply, $url, $skip) = (undef, opt($use, $h), '');
-+ $arg = '' unless $arg;
-+
-+ if ($use eq 'ip') {
-+ $ip = opt('ip', $h);
-+ $arg = 'ip';
-+
-+ } elsif ($use eq 'if') {
-+ $skip = opt('if-skip', $h) || '';
-+ $reply = `ip -6 addr list dev "$arg" scope global -tentative -deprecated -dadfailed | grep inet6 | grep -v temporary | grep -v "inet6 f[cd]" 2> /dev/null`;
-+ $reply = '' if $?;
-+ }
-+ if (!defined $reply) {
-+ $reply = '';
-+ }
-+ if ($skip) {
-+ $skip =~ s/ /\\s/is;
-+ $reply =~ s/^.*?${skip}//is;
-+ }
-+ if ($reply =~ /.*? ([0-9:][^\/]*)/i) {
-+ $ip = $1;
-+ }
-+ if (($use ne 'ip') && (define($ip,'') eq '0.0.0.0')) {
-+ $ip = undef;
-+ }
-+
-+ debug("get_ipv6: using %s, %s reports %s", $use, $arg, define($ip, ""));
-+ return $ip;
-+}
-+
- ######################################################################
- ## ipv6_match determine ipv6 address from given string and return them
- ######################################################################
diff -Nru ddclient-3.9.1/debian/po/pt_PT.po ddclient-3.10.0/debian/po/pt_PT.po
--- ddclient-3.9.1/debian/po/pt_PT.po 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/po/pt_PT.po 2023-01-16 02:47:24.000000000 +0000
@@ -1,22 +1,22 @@
-# ddclient Portuguese translation
-# Copyright (C) 2010 THE ddclient'S COPYRIGHT HOLDER
+# Translation of ddclient debconf messages to European Portuguese
+# Copyright (C) 2021 THE ddclient'S COPYRIGHT HOLDER
# This file is distributed under the same license as the ddclient package.
-# Flamarion Jorge , 2009, 2010.
-# José Vieira , 2020.
#
+# Américo Monteiro , 2021.
msgid ""
msgstr ""
"Project-Id-Version: ddclient 3.9.1-7\n"
"Report-Msgid-Bugs-To: ddclient@packages.debian.org\n"
"POT-Creation-Date: 2020-08-05 18:23-0400\n"
-"PO-Revision-Date: 2020-08-20 20:06-0300\n"
-"Last-Translator: José Vieira \n"
-"Language-Team: Portuguese \n"
+"PO-Revision-Date: 2021-02-08 21:34+0000\n"
+"Last-Translator: Américo Monteiro \n"
+"Language-Team: Portuguese \n"
"Language: pt_PT\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Lokalize 2.0\n"
#. Type: select
#. Choices
@@ -33,44 +33,33 @@
#. Description
#: ../ddclient.templates:2002
msgid "Dynamic DNS service provider:"
-msgstr "Fornecedor de serviço de DNS dinâmico:"
+msgstr "Fornecedor do serviço Dynamic DNS:"
#. Type: select
#. Description
#: ../ddclient.templates:2002
-#| msgid ""
-#| "Please select the dynamic DNS service you are using. If the service
-you "
-#| "use is not listed, choose \"other\" and you will be asked for the "
-#| "protocol and the server name."
msgid ""
"Dynamic DNS service you are using. If the service you use is not listed, "
"choose \"other\"."
msgstr ""
-"Serviço de DNS dinâmico em uso. Se o serviço a usar
-não "
-"estiver na lista, escolher \"outro\"."
+"O serviço Dynamic DNS que está a usar. Se o serviço que você usa não estiver "
+"listado, escolha \"outro\"."
#. Type: string
#. Description
#: ../ddclient.templates:3001
-#| msgid "Dynamic DNS server:"
msgid "Dynamic DNS server (blank for default):"
-msgstr "Servidor de DNS dinâmico (em branco = predefinição):"
+msgstr "Servidor Dynamic DNS (vazio para predefinição):"
#. Type: string
#. Description
#: ../ddclient.templates:3001
-#| msgid ""
-#| "Please enter the name of the server which is providing you with
-dynamic "
-#| "DNS service (example: members.dyndns.org)."
msgid ""
"Server providing the dynamic DNS service (example: members.dyndns.org). "
"Leave blank to use the default for the \"${protocol}\" protocol."
msgstr ""
"Servidor que fornece o serviço de DNS dinâmico (exemplo: members.dyndns.org). "
-"Deixar em branco para usar a predefinição para o protocolo \"${protocol}\"."
+"Deixe vazio para usar o predefinido para o protocolo \"${protocol}\"."
#. Type: select
#. Description
@@ -78,147 +67,127 @@
#. Description
#: ../ddclient.templates:4002 ../ddclient.templates:5001
msgid "Dynamic DNS update protocol:"
-msgstr "Protocolo de actualização de DNS dinâmico:"
+msgstr "Protocolo de actualização do Dynamic DNS:"
#. Type: select
#. Description
#: ../ddclient.templates:4002
-#| msgid ""
-#| "Please select the dynamic DNS update protocol used by your dynamic DNS "
-#| "service provider."
msgid ""
"Dynamic DNS update protocol used by your dynamic DNS service provider. If "
"the protocol your service uses is not listed, select \"other\"."
msgstr ""
-"Protocolo de actualização de DNS dinâmico utilizado pelo fornecedor do "
-"serviço de DNS dinâmico. Se o protocolo usado pelo serviço não estiver "
-"na lista, seleccionar \"outro\"."
+"Protocolo de actualização de Dynamic DNS usado pelo seu fornecedor de "
+"serviço de DNS dinâmico. Se o protocolo que o seu serviço usa não está "
+"listado, selecione \"outro\"."
#. Type: string
#. Description
#: ../ddclient.templates:5001
-#| msgid ""
-#| "Please select the dynamic DNS update protocol used by your dynamic DNS "
-#| "service provider."
msgid ""
-"The name of the dynamic DNS update protocol used by your dynamic DNS
-service "
+"The name of the dynamic DNS update protocol used by your dynamic DNS service "
"provider."
msgstr ""
-"Nome do protocolo de actualização de DNS dinâmico utilizado pelo "
-"fornecedor do serviço de DNS dinâmico."
+"O nome do protocolo de actualização de DNS dinâmico usado pelo seu fornecedor "
+"de serviço de DNS dinâmico."
#. Type: string
#. Description
#: ../ddclient.templates:6001
msgid "Optional HTTP proxy:"
-msgstr "Proxy HTTP opcional:"
+msgstr "proxy HTTP opcional:"
#. Type: string
#. Description
#: ../ddclient.templates:6001
msgid ""
-"HTTP proxy in the form http://proxy.example.com
- or https://proxy.example
-."
-"com. Proxy authentication is not supported. Leave blank if you do not
-use an "
+"HTTP proxy in the form http://proxy.example.com or https://proxy.example."
+"com. Proxy authentication is not supported. Leave blank if you do not use an "
"HTTP proxy."
msgstr ""
-"Proxy HTTP no formato http://proxy.example.com
- ou https://proxy.example.com
-. "
-"Autenticação de proxy não suportada. Deixar em branco se não for usado nenhum "
-"proxy HTTP."
+"Proxy HTTP no formato http://proxy.example.com ou https://proxy.example."
+"com. A autenticação de proxy não é suportada. Deixe vazio se você não usar "
+"um proxy HTTP."
#. Type: string
#. Description
#: ../ddclient.templates:7001
msgid "Hosts to update (comma-separated):"
-msgstr "Hospedeiros a actualizar (separados por vírgulas):"
+msgstr "Máquinas a actualizar (separadas por vírgulas):"
#. Type: string
#. Description
#: ../ddclient.templates:7001
-#| msgid ""
-#| "Please enter the list of fully qualified domain names for the local "
-#| "host(s) (for instance, \"myname.dyndns.org\" with only one host or "
-#| "\"myname1.dyndns.org,myname2.dyndns.org\" for two hosts)."
msgid ""
"Comma-separated list of fully qualified domain names to update (for "
-"instance, \"myname.dyndns.org\" with only one host, or
-\"myname1.dyndns.org,"
+"instance, \"myname.dyndns.org\" with only one host, or \"myname1.dyndns.org,"
"myname2.dyndns.org\" for two hosts)."
msgstr ""
-"Lista de nomes de domínio totalmente qualificados separados por vírgulas
-(por "
-"exemplo, \"meunome.dyndns.org\" para um hospedeiro ou "
-"\"meunome1.dyndns.org,meunome2.dyndns.org\" para dois hospedeiros)."
+"Lista separada por vírgulas dos nomes de domínio totalmente qualificados "
+"para actualizar (por exemplo \"myname.dyndns.org\" com apenas uma máquina, "
+"ou \"myname1.dyndns.org,myname2.dyndns.org\" para duas máquinas)."
#. Type: string
#. Description
#: ../ddclient.templates:8001
msgid "Username:"
-msgstr "Utilizador:"
+msgstr "Nome de utilizador:"
#. Type: string
#. Description
#: ../ddclient.templates:8001
-#| msgid "Please enter the username to use with the dynamic DNS service."
msgid ""
"Username (or other type of account identifer) to use with the dynamic DNS "
"service."
msgstr ""
-"Nome de utilizador (ou outro tipo de identificador de conta) para o serviço "
-"de DNS dinâmico."
+"Nome de utilizador (ou outro tipo de identificador de conta) para usar "
+"com o serviço de DNS dinâmico."
#. Type: password
#. Description
#: ../ddclient.templates:9001
msgid "Password:"
-msgstr "Senha:"
+msgstr "Palavra passe:"
#. Type: password
#. Description
#: ../ddclient.templates:9001
-#| msgid "Please enter the password to use with the dynamic DNS service."
msgid "Password, API key, or token to use with the dynamic DNS service."
-msgstr "Senha, chave de API ou código para o serviço de DNS dinâmico."
+msgstr ""
+"Palavra passe, chave API, ou testemunho a usar com o serviço de DNS dinâmico."
#. Type: password
#. Description
#: ../ddclient.templates:10001
msgid "Re-enter password:"
-msgstr "Reintroduzir a senha:"
+msgstr "Re-insira a palavra-passe:"
#. Type: password
#. Description
#: ../ddclient.templates:10001
-#| msgid "Please enter the password to use with the dynamic DNS service."
msgid ""
-"Password, API key, or token entered again to ensure it was entered
-correctly."
+"Password, API key, or token entered again to ensure it was entered correctly."
msgstr ""
-"Senha, chave de API ou código introduzido novamente para garantir que foi "
-"introduzido corretamente."
+"Palavra passe, chave API, ou testemunho inserido outra vez para assegurar "
+"que foi inserido corretamente."
#. Type: error
#. Description
#: ../ddclient.templates:11001
msgid "Passwords do not match"
-msgstr "As senhas não coincidem"
+msgstr "As palavras-passe não correspondem"
#. Type: error
#. Description
#: ../ddclient.templates:11001
msgid "The two passwords you entered were not the same. Please try again."
-msgstr "As senhas introduzidas não são iguais. Voltar a tentar."
+msgstr ""
+"As duas palavras-passe que inseriu não são iguais. Por favor tente de novo."
#. Type: select
#. Choices
#: ../ddclient.templates:12001
msgid "Web-based IP discovery service"
-msgstr "Serviço de descoberta de IP baseado na web"
+msgstr "Serviço de descoberta de IP baseado em Web"
#. Type: select
#. Choices
@@ -236,48 +205,44 @@
#. Description
#: ../ddclient.templates:12002
msgid ""
-"The method ddclient uses to determine your current IP address. Your
-options:"
+"The method ddclient uses to determine your current IP address. Your options:"
msgstr ""
-"Método que o ddclient usa para determinar o endereço IP em uso. Opções:"
+"O método que o ddclient usa para determinar o seu endereço IP actual. A suas "
+"opções:"
#. Type: select
#. Description
#: ../ddclient.templates:12002
msgid ""
"Web-based IP discovery service: Periodically visit a web page that shows "
-"your IP address. You probably want this option if your computer is
-connected "
+"your IP address. You probably want this option if your computer is connected "
"to the Internet via a Network Address Translation (NAT) device such as a "
"typical consumer router."
msgstr ""
-"Serviço de descoberta de IP baseado na web: visita periodicamente uma "
-"página web que mostra o endereço IP. Provavelmente será esta a
-opção a escolher "
-"se o computador estiver ligado à Internet através dum dispositivo NAT "
-"(Network Address Translation), como seja um encaminhador (router) doméstico comum."
+"Serviço de descoberta de IP baseado em Web: Periodicamente visita uma "
+"página web que mostra o seu endereço IP. Provavelmente você vai querer esta "
+"opção se o seu computador estiver ligado à Internet via dispositivo de "
+"Network Address Translation (NAT) tal como um router típico de consumidor."
#. Type: select
#. Description
#: ../ddclient.templates:12002
msgid ""
"Network interface: Use the IP address assigned to your computer's network "
-"interface (such as an Ethernet adapter or PPP connection). You probably
-want "
+"interface (such as an Ethernet adapter or PPP connection). You probably want "
"this option if your computer connects directly to the Internet (your "
"connection does not go through a NAT device)."
msgstr ""
-"Interface de rede: usar o endereço IP associado à interface de rede do "
-"computador (por exemplo um adaptador Ethernet ou ligação PPP). "
-"Provavelmente será esta a opção a escolher se o computador estiver ligado
-directamente "
-"à Internet (a ligação não passa por um dispositivo NAT)."
+"Interface de rede: Use o endereço IP atribuído à interface de rede do seu "
+"computador (tal como uma placa Ethernet ou ligação PPP). Provavelmente "
+"você vai querer esta opção se o seu computador estiver ligado directamente "
+"à Internet (a sua ligação não passa por um dispositivo NAT)."
#. Type: select
#. Description
#: ../ddclient.templates:13002
msgid "IP discovery service:"
-msgstr "Serviço de descoberta IP:"
+msgstr "Serviço de descoberta de IP:"
#. Type: select
#. Description
@@ -286,8 +251,8 @@
"The web-based IP discovery service you would like ddclient to use to "
"determine your current IP address."
msgstr ""
-"Serviço de descoberta de IP baseado na web a usar pelo ddclient para "
-"descobrir qual é o endereço IP em uso."
+"O serviço de descoberta de IP baseado em web que você deseja que o ddclient "
+"use para determinar o seu endereço IP actual."
#. Type: string
#. Description
@@ -299,7 +264,7 @@
#. Description
#: ../ddclient.templates:14001
msgid "URL to a web page that returns your IP address."
-msgstr "URL para uma página web que mostra o endereço IP."
+msgstr "URL para uma página web que retorna o seu endereço IP."
#. Type: string
#. Description
@@ -314,45 +279,42 @@
"The name of the network interface (e.g., eth0, wlan0, ppp0) that ddclient "
"will look at to determine the current IP address."
msgstr ""
-"Nome da interface de rede (p. ex., eth0, wlan0, ppp0) que o ddclient
-usará "
-"para descobrir qual é o endereço IP em uso."
+"O nome da interface de rede (ex. eth0, wlan0, ppp0) que o ddclient irá "
+"olhar para determinar o endereço IP actual."
#. Type: select
#. Choices
#: ../ddclient.templates:16001
msgid "As a daemon"
-msgstr "Como serviço (daemon)"
+msgstr "Como um daemon"
#. Type: select
#. Choices
#: ../ddclient.templates:16001
-#| msgid "Run ddclient on PPP connect?"
msgid "On PPP connect"
-msgstr "Ao ligar por PPP"
+msgstr "Em ligação PPP"
#. Type: select
#. Description
#: ../ddclient.templates:16002
msgid "How to run ddclient:"
-msgstr "Como executar o ddclient:"
+msgstr "Como correr o ddclient:"
#. Type: select
#. Description
#: ../ddclient.templates:16002
msgid "The ddclient run mode. Your options:"
-msgstr "Modo de execução do ddclient. Opções:"
+msgstr "O modo de execução do ddclient. As suas opções:"
#. Type: select
#. Description
#: ../ddclient.templates:16002
msgid ""
-"As a daemon: ddclient runs in the background periodically checking to
-see if "
+"As a daemon: ddclient runs in the background periodically checking to see if "
"the IP address has changed."
msgstr ""
-"Como serviço (deamon): o ddclient é executado em segundo plano, verificando "
-"periodicamente se o endereço IP mudou."
+"Como um daemon: ddclient corre em segundo plano verificando periodicamente "
+"para ver se o seu endereço IP mudou."
#. Type: select
#. Description
@@ -361,42 +323,32 @@
"On PPP connect: Each time you connect via PPP ddclient will start, update "
"the IP address, and exit."
msgstr ""
-"Ao ligar por PPP: de cada vez que for feita uma ligação via PPP, o ddclient vai "
-"iniciar, actualizar o endereço IP e sair."
+"Em ligação PPP: Cada vez que você liga via PPP o ddclient irá arrancar, "
+"actualiza o endereço IP, e termina."
#. Type: string
#. Description
#: ../ddclient.templates:17001
msgid "Time between address checks:"
-msgstr "Intervalo entre verificações de endereço:"
+msgstr "Tempo entre verificações de endereço:"
#. Type: string
#. Description
#: ../ddclient.templates:17001
-#| msgid ""
-#| "Please choose the delay between interface address checks. Values may
-be "
-#| "given in seconds (e.g. \"5s\"), in minutes (e.g. \"3m\"), in hours
-(e.g. "
-#| "\"7h\") or in days (e.g. \"1d\")."
-msgid ""
-"How long ddclient should wait between IP address checks. Values may be
-given "
-"in seconds (e.g., \"300s\"), in minutes (e.g., \"5m\"), in hours (e.g.,
-\"7h"
+msgid ""
+"How long ddclient should wait between IP address checks. Values may be given "
+"in seconds (e.g., \"300s\"), in minutes (e.g., \"5m\"), in hours (e.g., \"7h"
"\") or in days (e.g., \"1d\")."
msgstr ""
-"Intervalo entre verificações de endereço IP. Os valores podem ser "
-"dados
-em segundos (p. ex., \"300s\"), em minutos (p. ex., \"5m\"), em horas "
-"(p. ex., \"7h"
-"\") ou em dias (p. ex., \"1d\")."
+"Quando tempo deve o ddclient esperar entre verificações de endereço IP. "
+"Valores podem ser dados em segundo (ex. \"300s\"), em minutos (ex. \"5m\"), "
+"em horas (ex, \"7h\") ou em dias (ex. \"1d\")."
#. Type: select
#. Choices
#: ../ddclient.templates:18001
msgid "From list"
-msgstr "Da lista"
+msgstr "A partir de lista"
#. Type: select
#. Choices
@@ -408,58 +360,56 @@
#. Description
#: ../ddclient.templates:18002
msgid "How to enter host names:"
-msgstr "Como introduzir nomes de hospedeiro:"
+msgstr "Como inserir nomes de máquinas:"
#. Type: select
#. Description
#: ../ddclient.templates:18002
msgid "How to prompt you for the host name(s) that ddclient will update."
-msgstr "Como solicitar o(s) nome(s) de hospedeiro a actualizar."
+msgstr ""
+"Como lhe perguntar pelos nome(s) de máquina(s) que o ddclient irá actualizar."
#. Type: select
#. Description
#: ../ddclient.templates:18002
msgid ""
-"If you choose \"From list\", this program will attempt to look up the
-host "
+"If you choose \"From list\", this program will attempt to look up the host "
"names that are registered with your DynDNS account. You will then select "
"hosts from that list."
msgstr ""
-"Escolhendo \"Da lista\", o programa procurará os nomes
-de "
-"hospedeiro registados na conta DynDNS. Terão que ser seleccionados "
-"hospedeiros dessa lista."
+"Se você escolher \"A partir de lista\", este programa irá tentar encontrar "
+"os nomes de máquinas que estão registados com a sua conta DynDNS. Você "
+"irá então selecionar máquinas dessa lista."
#. Type: select
#. Description
#: ../ddclient.templates:18002
-msgid "If you choose \"Manually\", you will have to type in the host
-name(s)."
-msgstr "Escolhendo \"Manualmente\", terão que ser introduzidos os nomes de hospedeiro."
+msgid "If you choose \"Manually\", you will have to type in the host name(s)."
+msgstr ""
+"Se você escolher \"Manualmente\", você terá de escrever os nomes das"
+" máquina(s)."
#. Type: multiselect
#. Description
#: ../ddclient.templates:19001
-#| msgid "Host names to keep updated:"
msgid "Hosts to update:"
-msgstr "Hospedeiros a actualizar:"
+msgstr "Máquinas para actualizar:"
#. Type: multiselect
#. Description
#: ../ddclient.templates:19001
msgid ""
-"The host name(s) to keep updated with your current IP address. (This
-list of "
+"The host name(s) to keep updated with your current IP address. (This list of "
"host names was downloaded from your DynDNS account.)"
msgstr ""
-"Nome(s) de hospedeiro a manter actualizado(s) com o endereço IP em "
-"uso. (Esta lista de hospedeiros foi transferida da conta DynDNS)."
+"Os nome(s) de máquinas para manter actualizadas com o seu endereço IP actual. "
+"(Esta lista de nomes de máquinas foi descarregada da sua conta DynDNS.)"
#. Type: error
#. Description
#: ../ddclient.templates:20001
msgid "Empty host list"
-msgstr "Lista de hospedeiros vazia"
+msgstr "Lista de máquinas vazia"
#. Type: error
#. Description
@@ -468,8 +418,8 @@
"The list of host names managed under your account is empty when retrieved "
"from the dynamic DNS service website."
msgstr ""
-"Quando obtida do sítio web do serviço de DNS dinâmico, "
-"a lista de hospedeiros estará vazia."
+"A lista de nomes de máquinas geridas sob a sua conta estava vazia quando "
+"obtida a partir do sítio web do serviço de DNS dinâmico."
#. Type: error
#. Description
@@ -478,181 +428,17 @@
"You may have provided an incorrect username or password, or the online "
"account may have no host names configured."
msgstr ""
-"O nome de utilizador ou a senha podem ter sido incorretamente introduzidos,
-ou a conta pode não ter os nomes de hospedeiro configurados."
+"Você pode ter fornecido um nome de utilizador ou palavra passe incorretos, "
+"ou a conta online pode não ter nomes de máquinas configurados."
#. Type: error
#. Description
#: ../ddclient.templates:20001
msgid ""
"Please check your account to be sure you have host names configured, then "
-"run \"dpkg-reconfigure ddclient\" to input your username and password
-again."
+"run \"dpkg-reconfigure ddclient\" to input your username and password again."
msgstr ""
-"Confirmar que tem nomes de hospedeiro configurados na conta; de seguida "
-"executar \"dpkg-reconfigure ddclient\" para introduzir novamente
-o utilizador e a senha."
-
-#, fuzzy
-#~| msgid ""
-#~| "You should enable this option if ddclient should be run every time
-a PPP "
-#~| "connection is established. Note: This mode is not compatible with
-daemon "
-#~| "mode."
-#~ msgid ""
-#~ "Whether ddclient should run every time a PPP connection is
-established. "
-#~ "Note: ddclient will not run as a daemon if this is enabled."
-#~ msgstr ""
-#~ "No caso de o ddclient dever ser executado de cada vez
-que uma ligação PPP for estabelecida. "
-#~ "Nota: O ddclient não será executado no modo de serviço (daemon) "
-#~ "se esta opção for activada."
-
-#~ msgid "Find public IP using checkip.dyndns.com?"
-#~ msgstr "Procurar IP público usando checkip.dyndns.com?"
-
-#, fuzzy
-#~| msgid ""
-#~| "Please choose whether ddclient should try to find the IP address of
-this "
-#~| "machine via the DynDNS web interface. This is recommended for
-machines "
-#~| "that are using Network Address Translation."
-#~ msgid ""
-#~ "Whether ddclient should try to find the IP address of this machine via "
-#~ "the DynDNS web interface. This is recommended for machines that are
-using "
-#~ "Network Address Translation."
-#~ msgstr ""
-#~ "No caso de o ddclient dever tentar procurar o endereço IP desta máquina através
-da interface "
-#~ "web DynDNS. Recomendado para máquinas que estiverem usando "
-#~ "Network Address Translation."
-
-#~ msgid "Run ddclient as daemon?"
-#~ msgstr "Executar o ddclient como serviço?"
-
-#, fuzzy
-#~| msgid ""
-#~| "Please choose whether you want ddclient to be run in daemon mode on "
-#~| "system startup."
-#~ msgid "Whether ddclient should run in daemon mode on system startup."
-#~ msgstr ""
-#~ "No caso de o ddclient dever ser executado em modo de serviço (daemon) "
-#~ "no arranque do sistema."
-
-#~ msgid ""
-#~ "The list of host names managed via your DynDNS account has been "
-#~ "downloaded. Please choose the one(s) for which ddclient should be
-used to "
-#~ "keep IP address records up to date."
-#~ msgstr ""
-#~ "A lista de nomes de hospedeiro geridas pela conta DynDNS foi "
-#~ "trasferida. Escolher o(s) nome(s) para os quais deve ser usado "
-#~ "o ddclient para manter os registos de endereços IP actualizados."
-
-#~ msgid "Selection method for updated names:"
-#~ msgstr "Método de selecção para nomes actualizados:"
-
-#~ msgid ""
-#~ "You'll have to select which host names to update using ddclient.
-You can "
-#~ "select host names to update from a list (taken from your DynDNS
-account) "
-#~ "or enter them manually."
-#~ msgstr ""
-#~ "Terão que ser seleccionados os nomes de hospedeiro a actualizar "
-#~ "usando o ddclient. "
-#~ "Os nomes podem ser seleccionados de uma lista (obtidos da conta DynDNS) "
-#~ "ou introduzidos manualmente."
-
-#~ msgid "Interval between ddclient runs:"
-#~ msgstr "Intervalo entre execuções do ddclient:"
-
-#~ msgid "Network interface used for dynamic DNS service:"
-#~ msgstr "Interface de rede usada para o serviço de DNS dinâmico:"
-
-#~ msgid ""
-#~ "Please enter the name of the network interface (eth0/wlan0/ppp0/...)
-to "
-#~ "use for dynamic DNS service."
-#~ msgstr ""
-#~ "Introduzir o nome da interface de rede (eth0/wlan0/ppp0/...) a
-usar "
-#~ "para o serviço de DNS dinâmico."
-
-#~ msgid "Password for dynamic DNS service:"
-#~ msgstr "Senha para o serviço de DNS dinâmico:"
-
-#~ msgid "Username for dynamic DNS service:"
-#~ msgstr "Utilizador para o serviço de DNS dinâmico:"
-
-#~ msgid "DynDNS fully qualified domain names:"
-#~ msgstr "Nomes de domínio DynDNS totalmente qualificados (FQDN):"
-
-#, fuzzy
-#~| msgid "Run ddclient on PPP connect?"
-#~ msgid "Run ddclient on DHCP update?"
-#~ msgstr "Executar o ddclient na actualização DHCP?"
-
-#, fuzzy
-#~| msgid ""
-#~| "You should enable this option if ddclient should be run every time
-a PPP "
-#~| "connection is established. Note: This mode is not compatible with
-daemon "
-#~| "mode."
-#~ msgid ""
-#~ "You should enable this option if ddclient should be run every time
-the IP "
-#~ "address is changed by the DHCP client (dhclient). Note: This mode is
-not "
-#~ "compatible with daemon mode."
-#~ msgstr ""
-#~ "Activar esta opção para executar o ddclient de cada vez
-que "
-#~ "uma ligação PPP for estabelecida. Nota: Este modo não é compatível
-com o "
-#~ "modo de serviço (daemon)."
-
-#~ msgid "Modified configuration file"
-#~ msgstr "Ficheiro de configuração modificado"
-
-#~ msgid ""
-#~ "The config file /etc/ddclient.conf on your system does not consist of "
-#~ "three entries. The automatic configuration utility cannot handle this "
-#~ "situation."
-#~ msgstr ""
-#~ "O ficheiro de configuração /etc/ddclient.conf no sistema não
-consiste "
-#~ "de três entradas. O utilitário de configuração automática não consegue
-lidar "
-#~ "com isso."
-
-#~ msgid ""
-#~ "If you have edited the configuration file manually, it won't be
-modified. "
-#~ "If you need a new configuration file, run \"dpkg-reconfigure
-ddclient\"."
-#~ msgstr ""
-#~ "Se o ficheiro de configuração foi editado manualmente, ele não será "
-#~ "modificado. Se for necessário um novo ficheiro de configuração,
-executar "
-#~ "\"dpkg-reconfigure ddclient\"."
-
-#~ msgid "www.dyndns.com "
-#~ msgstr "www.dyndns.com "
-
-#~ msgid "www.easydns.com "
-#~ msgstr "www.easydns.com "
-
-#~ msgid "www.dslreports.com "
-#~ msgstr "www.dslreports.com "
-
-#~ msgid "www.zoneedit.com "
-#~ msgstr "www.zoneedit.com "
+"Por favor verifique a sua conta para certificar que tem nomes de máquinas "
+"configurados, depois corra \"dpkg-reconfigure ddclient\" para inserir o seu "
+"nome de utilizador e palavra passe outra vez."
-#~ msgid "ddclient update interval:"
-#~ msgstr "Intervalo de actualização do ddclient:"
diff -Nru ddclient-3.9.1/debian/postinst ddclient-3.10.0/debian/postinst
--- ddclient-3.9.1/debian/postinst 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/postinst 2023-01-16 02:47:24.000000000 +0000
@@ -122,8 +122,19 @@
# generated had those options not been passed.
case $1 in
configure|abort-upgrade|abort-deconfigure|abort-remove)
- #### Begin 1st part generated by dh_installsystemd/13.1
- # This will only remove masks created by d-s-h on package removal.
+ #### Begin part generated by dh_installinit/13.11.1
+ if [ -z "${DPKG_ROOT:-}" ] && [ -x "/etc/init.d/ddclient" ]; then
+ update-rc.d ddclient defaults >/dev/null
+ if [ -n "$2" ]; then
+ _dh_action=restart
+ else
+ _dh_action=start
+ fi
+ invoke-rc.d --skip-systemd-native ddclient $_dh_action || exit 1
+ fi
+ #### End part generated by dh_installinit/13.11.1
+ #### Begin 1st part generated by dh_installsystemd/13.11.1
+ # The following line should be removed in trixie or trixie+1
deb-systemd-helper unmask 'ddclient.service' >/dev/null || true
# was-enabled defaults to true, so new installations run enable.
@@ -136,19 +147,18 @@
# cleaned up on purge. Also remove old symlinks.
deb-systemd-helper update-state 'ddclient.service' >/dev/null || true
fi
- #### End 1st part generated by dh_installsystemd/13.1
- #### Begin 2nd part generated by dh_installsystemd/13.1
+ #### End 1st part generated by dh_installsystemd/13.11.1
+ #### Begin 2nd part generated by dh_installsystemd/13.11.1
if [ -d /run/systemd/system ]; then
systemctl --system daemon-reload >/dev/null || true
- deb-systemd-invoke start 'ddclient.service' >/dev/null || true
- fi
- #### End 2nd part generated by dh_installsystemd/13.1
- #### Begin part generated by dh_installinit/13.1
- if [ -x "/etc/init.d/ddclient" ]; then
- update-rc.d ddclient defaults >/dev/null
- invoke-rc.d ddclient start || exit 1
+ if [ -n "$2" ]; then
+ _dh_action=restart
+ else
+ _dh_action=start
+ fi
+ deb-systemd-invoke $_dh_action 'ddclient.service' >/dev/null || true
fi
- #### End part generated by dh_installinit/13.1
+ #### End 2nd part generated by dh_installsystemd/13.11.1
;;
esac
else
diff -Nru ddclient-3.9.1/debian/prerm ddclient-3.10.0/debian/prerm
--- ddclient-3.9.1/debian/prerm 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/prerm 2023-01-16 02:47:24.000000000 +0000
@@ -14,15 +14,15 @@
# to make it possible to conditionally enable the service. The
# --no-start option also disables stopping the service on remove, so
# it has to be manually stopped here.
-#### Begin part generated by dh_installsystemd/13.1
-if [ -d /run/systemd/system ]; then
+#### Begin part generated by dh_installsystemd/13.11.1
+if [ -z "${DPKG_ROOT:-}" ] && [ "$1" = remove ] && [ -d /run/systemd/system ] ; then
deb-systemd-invoke stop 'ddclient.service' >/dev/null || true
fi
-#### End part generated by dh_installsystemd/13.1
-#### Begin part generated by dh_installinit/13.1
-if [ -x "/etc/init.d/ddclient" ]; then
- invoke-rc.d ddclient stop || exit 1
+#### End part generated by dh_installsystemd/13.11.1
+#### Begin part generated by dh_installinit/13.11.1
+if [ -z "${DPKG_ROOT:-}" ] && [ "$1" = remove ] && [ -x "/etc/init.d/ddclient" ] ; then
+ invoke-rc.d --skip-systemd-native ddclient stop || exit 1
fi
-#### End part generated by dh_installinit/13.1
+#### End part generated by dh_installinit/13.11.1
exit 0
diff -Nru ddclient-3.9.1/debian/rules ddclient-3.10.0/debian/rules
--- ddclient-3.9.1/debian/rules 2020-09-08 18:24:00.000000000 +0000
+++ ddclient-3.10.0/debian/rules 2023-01-16 02:47:24.000000000 +0000
@@ -2,6 +2,22 @@
%:
dh $@
+# postinst generates /etc/ddclient.conf via debconf. Move the default
+# /etc/ddclient.conf to the examples directory to prevent it from interfering.
+#
+# TODO: It seems like it should be possible to use dh_install to move the file,
+# but I couldn't figure out how to make it work. Adding the following line to
+# debian/ddclient.install copies the file rather than renames it (as of
+# debhelper 13):
+#
+# debian/ddclient/etc/ddclient.conf => /usr/share/doc/ddclient/examples/sample-etc_ddclient.conf
+#
+# dh_installexamples similarly copies the file (and it doesn't provide a way to
+# rename it).
+execute_after_dh_auto_install:
+ install -d debian/ddclient/usr/share/doc/ddclient/examples
+ mv debian/ddclient/etc/ddclient.conf debian/ddclient/usr/share/doc/ddclient/examples/sample-etc_ddclient.conf
+
# This package conditionally enables the ddclient service based on
# debconf answers.
#
diff -Nru ddclient-3.9.1/docs/ipv6-design-doc.md ddclient-3.10.0/docs/ipv6-design-doc.md
--- ddclient-3.9.1/docs/ipv6-design-doc.md 1970-01-01 00:00:00.000000000 +0000
+++ ddclient-3.10.0/docs/ipv6-design-doc.md 2022-10-20 18:06:35.000000000 +0000
@@ -0,0 +1,337 @@
+# Design Doc: IPv6 Support
+
+Author: [@rhansen](https://github.com/rhansen/)\
+Date: 2020-06-09\
+Signed off by:
+[@SuperSandro2000](https://github.com/SuperSandro2000/)
+
+## Objective
+
+Add full IPv6 support to ddclient, including support for dual-stack
+systems.
+
+## Background
+
+ddclient's current IPv6 support is limited:
+
+ * Users can update either an IPv6 record or an IPv4 record for a
+ host, not both.
+ * If SSL is used for an HTTP request, IPv6 will be used if the
+ remote host has a AAAA record, even if the user would rather use
+ IPv4. This breaks `use=web` for IPv4 if the `web` URL's host has a
+ AAAA record.
+ * The `use=if` method only works if the user sets `if-skip` to
+ something that skips over all IPv4 addresses in the output of
+ `ifconfig` (or `ip`). If the output contains an IPv4 address after
+ the IPv6 address then `use=if` cannot be used for IPv6.
+ * There is no support for falling back to IPv4 if an IPv6 connection
+ fails.
+ * `use=if` does not filter out locally scoped or temporary IPv6
+ addresses.
+
+Some attempts have been made to add more robust IPv6 support:
+
+ * Debian's ddclient package applies a
+ [patch](https://salsa.debian.org/debian/ddclient/-/blob/67a138aa3d98d70f01766123f58ef40e98693fd4/debian/patches/usev6.diff)
+ that adds a new `usev6` option. The `usev6` option can be set to
+ `ip` or `if`, but not any of the other strategies currently
+ available for the `use` option (`web`, `cmd`, `fw`, `cisco`,
+ `cisco-asa`). When set to `ip` or `if`, only IPv6 addresses are
+ considered; IPv4 addresses are ignored. The patch does not change
+ the behavior of the `use` option, so `use=web` or `use=cmd` can be
+ used for IPv6 if pointed at something that only outputs an IPv6
+ address.
+ * [ddclient-curl](https://github.com/astlinux-project/ddclient-curl)
+ is a fork of ddclient that uses curl as the HTTP client (instead
+ of ddclient's own homemade client) for more robust IPv6 support.
+ * PR #40 is perhaps the most comprehensive attempt at adding full
+ IPv6 support, but it was never merged and has since
+ bit-rotted. There is renewed effort to rebase the changes and get
+ them merged in. PR #40 adds new options and changes some existing
+ options. The approach taken is to completely isolate IPv4 address
+ detection from IPv6 address detection and require the update
+ protocol callbacks to handle each type of address appropriately.
+
+## Requirements
+
+ * The mechanism for determining the current IPv4 address (the `use`
+ option) must be independently configurable from the mechanism used
+ to determine the current IPv6 address.
+ * The user must be able to disable IPv4 address updates without
+ affecting IPv6 updates.
+ * The user must be able to disable IPv6 address updates without
+ affecting IPv4 updates.
+ * If HTTP polling is used for both IPv4 and IPv6 address discovery,
+ the URL used to determine the IPv4 address (the `web` option) must
+ be independently configurable from the URL used to determine the
+ IPv6 address.
+ * The use of IPv4 or IPv6 to update a record must be independent of
+ the type of record being updated (IPv4 or IPv6).
+ * The callback for the update protocol must be given both addresses,
+ even if only one of the two addresses has changed.
+ * The callback for the update protocol must be told which addresses
+ have changed.
+ * There must be IPv6 equivalents to `use=ip`, `use=if`, `use=web`,
+ and `use=cmd`. For the IPv6 equivalent to `use=if`, it is
+ acceptable to ignore non-global and temporary addresses (the user
+ can always use the IPv6 equivalent to `use=cmd` to get non-global
+ or temporary addresses).
+ * Existing support for updating IPv6 records must not be lost.
+ * Some dynamic DNS service providers use separate credentials for
+ the IPv4 and IPv6 records. These providers must be supported,
+ either by accepting both sets of credentials in a single host's
+ configuration or by allowing the user to specify the same host
+ twice, once for IPv4 and once for IPv6.
+
+### Nice-to-Haves
+
+ * The user should be able to force the update protocol to use IPv4
+ or IPv6.
+ * Unless configured otherwise, ddclient should first attempt to
+ update via IPv6 and fall back to IPv4 if the IPv6 connection
+ fails. This behavior can be added later; for now it is acceptable
+ to keep the current behavior (use IPv6 without IPv4 fallback if
+ there is a AAAA record, use IPv4 if there is no AAAA record).
+ * Full backwards compatibility with existing config files and
+ flags. The trade-offs between migration burden, long-term
+ usability, and code maintenance should be carefully considered.
+ * IPv6 equivalents to `use=fw`, `use=cisco`, and `use=cisco-asa`.
+ * Add IPv6 support in protocol callbacks where IPv6 support is
+ currently missing. (This can be done later.)
+
+## Proposal
+
+### Configuration changes
+
+ * Add new `usev4` and `usev6` settings that are like the current
+ `use` setting except they only apply to IPv4 and IPv6,
+ respectively.
+ * `usev4` can be set to one of the following values: `disabled`,
+ `ipv4`, `webv4`, `fwv4`, `ifv4`, `cmdv4`, `ciscov4`,
+ `cisco-asav4`
+ * `usev6` can be set to one of the following values: `disabled`,
+ `ipv6`, `webv6`, `fwv6`, `ifv6`, `cmdv6`, `ciscov6`,
+ `cisco-asav6`
+ * Add a new `use` strategy: `disabled`.
+ * The `disabled` value for `use`, `usev4`, and `usev6` causes
+ ddclient to act as if it was never set. This is useful for
+ overriding the global value for a particular host.
+ * For compatibility with ddclient-curl, `no` is a deprecated alias
+ of `disabled`.
+ * Add new `ipv4`, `ipv6`, `webv4`, `webv4-skip`, `webv6`,
+ `webv6-skip`, `ifv4`, `ifv6`, `cmdv4`, `cmdv6`, etc. settings that
+ behave like their versionless counterparts except they only apply
+ to IPv4 or IPv6. Deprecate the versionless counterparts, and
+ change their behavior so that they also influence the default
+ value of the versioned options. (Example: Suppose
+ `usev4=ifv4`. If `ifv4` is not set then `if` is used.) Special
+ notes:
+ * The value of `ip` will only serve as the default for `ipv4`
+ (or `ipv6`) if it contains an IPv4 (or IPv6) address.
+ * There is currently an `ipv6` boolean setting. To preserve
+ backward compatibility with existing configs, `ipv6` set to a
+ boolean value is ignored (other than a warning).
+ * There is no `ifv4-skip` or `ifv6-skip` because it's ddclient's
+ responsibility to properly parse the output of whatever tool
+ it uses to read the interface's addresses.
+ * For now there is no `cmdv4-skip` or `cmdv6-skip`. Anyone who
+ already knows how to write a regular expression can probably
+ write a wrapper script. These may be added in the future if
+ users request them, especially if it facilitates migration
+ away from the deprecated `cmd-skip` setting.
+ * For `usev6=ifv6`, interfaces are likely to have several IPv6
+ addresses (unlike IPv4). Choosing the "right" IPv6 address is
+ not trivial. Fortunately, we don't have to solve this
+ perfectly right now; we can choose something that mostly
+ works and let user bug reports guide future refinements. For
+ the first iteration, we will try the following:
+ * Ignore addresses that are not global unicast.
+ (Unfortunately, the `ip` command from iproute2 does not
+ provide a way to filter out ULA addresses so we will have
+ to do this ourselves.)
+ * Ignore temporary addresses.
+ * If no addresses remain, log a warning and don't update the
+ IPv6 record.
+ * Otherwise, if one of the remaining addresses matches the
+ previously selected address, continue to use it.
+ * Otherwise, select one arbitrarily.
+ * Deprecate the `use` setting (print a loud warning) but keep its
+ existing semantics with an exception: If there is a conflict with
+ `usev4` or `usev6` then those take priority:
+ * If `use`, `usev4`, and `usev6` are all set then a warning is
+ logged and the `use` setting is ignored.
+ * If `use` and `usev4` are both set and the `use` strategy
+ discovers an IPv4 address that differs from the address
+ discovered by the `usev4` strategy, then the address from
+ `usev4` is used and a warning is logged.
+ * If `use` and `usev6` are both set and the `use` strategy
+ discovers an IPv6 address that differs from the address
+ discovered by the `usev6` strategy, then the address from
+ `usev6` is used and a warning is logged.
+ * If `usev4` (`usev6`) is not set:
+ * If `ipv4` (`usev6`) is set, ddclient acts as if `usev4`
+ (`usev6`) was set to `ipv4` (`ipv6`).
+ * Otherwise, if `ifv4` (`ifv6`) is set, ddclient acts as if
+ `usev4` (`usev6`) was set to `ifv4` (`ifv6`).
+ * Otherwise, if `cmdv4` (`cmdv6`) is set, ddclient acts as if
+ `usev4` (`usev6`) was set to `cmdv4` (`cmdv6`).
+ * Otherwise, if `fwv4` (`fwv6`) is set, ddclient acts as if
+ `usev4` (`usev6`) was set to `fwv4` (`fwv6`).
+ * Otherwise, `usev4` (`usev6`) remains unset.
+ * To support separate credentials for IPv4 vs. IPv6 updates, users
+ can specify the same host multiple times, each time with different
+ options.
+
+### Internal API changes
+
+ * Add two new entries to the `$config{$host}` hash:
+ * `$config{$host}{'wantipv4'}` is set to:
+ * If `usev4` is enabled, the IPv4 address discovered by the
+ `usev4` strategy.
+ * Otherwise, if `use` is enabled and the `use` strategy
+ discovered an IPv4 address, the IPv4 address discovered by
+ the `use` strategy.
+ * Otherwise, `undef`.
+ * `$config{$host}{'wantipv6'}` is set to:
+ * If `usev6` is enabled, the IPv6 address discovered by the
+ `usev6` strategy.
+ * Otherwise, if `use` is enabled and the `use` strategy
+ discovered an IPv6 address, the IPv6 address discovered by
+ the `use` strategy.
+ * Otherwise, `undef`.
+ * Deprecate the existing `$config{$host}{'wantip'}` entry, to be
+ removed after all update protocol callbacks have been updated to
+ use the above new entries. In the meantime, this entry's value
+ depends on which of `use`, `usev4`, and `usev6` is enabled, and
+ what type of IP address is discovered by the `use` strategy (if
+ enabled), according to the following table:
+
+ | `use` | `usev4` | `usev6` | resulting value |
+ | :---: | :---: | :---: | :--- |
+ | ✔(IPv4) | ✖ | ✖ | the IPv4 address discovered by the `use` strategy |
+ | ✔(IPv6) | ✖ | ✖ | the IPv6 address discovered by the `use` strategy |
+ | ✖ | ✔ | ✖ | the IPv4 address discovered by the `usev4` strategy |
+ | ✖ | ✖ | ✔ | the IPv6 address discovered by the `usev6` strategy |
+ | ✔(IPv4) | ✔ | ✖ | the IPv4 address discovered by the `usev4` strategy (and log another warning if it doesn't match the IPv4 address found by the `use` strategy) |
+ | ✔(IPv6) | ✔ | ✖ | the IPv6 address discovered by the `use` strategy |
+ | ✔(IPv4) | ✖ | ✔ | the IPv4 address discovered by the `use` strategy |
+ | ✔(IPv6) | ✖ | ✔ | the IPv6 address discovered by the `usev6` strategy (and log another warning if it doesn't match the IPv6 address found by the `use` strategy) |
+
+ * To support separate credentials for IPv4 vs. IPv6 updates, convert
+ the `%config` hash of host configs into a list of host configs. A
+ second definition for the same host adds a second entry rather
+ than overwrites the existing entry.
+
+## Alternatives Considered
+
+### Repurpose the existing settings for v4
+
+Rather than create new `usev4`, `ifv4`, `cmdv4`, etc. settings,
+repurpose the existing `use`, `if`, `cmd`, etc. settings for IPv4.
+
+Why this was rejected:
+ * There is a usability advantage to the symmetry with the `v6`
+ settings.
+ * It is easier to remain compatible with existing configurations.
+
+### Let `use` set the default for `usev4`
+
+Rather than three separate IP discovery mechanisms (`use`, `usev4`,
+and `usev6`), have just two (`usev4` and `usev6`) and let the old
+`use` setting control the default for `usev4`: If `usev4` is not set,
+then `use=foo` is equivalent to `usev4=foov4`.
+
+Why this was rejected: Backwards incompatibility. Specifically,
+configurations that previously updated an IPv6 record would instead
+(attempt to) update an IPv4 record.
+
+### Let `use` set the default for `usev4` and `usev6`
+
+Rather than three separate IP discovery mechanisms (`use`, `usev4`,
+and `usev6`), have just two (`usev4` and `usev6`) and let the old
+`use` setting control the default for `usev4` and `usev6`:
+
+ * If neither `usev4` nor `usev6` is set, then `use=foo` is
+ equivalent to `usev4=foov4,usev6=foov6`.
+ * If `usev4` is set but not `usev6`, then `use=foo` is equivalent to
+ `usev6=foov6`.
+ * If `usev6` is set but not `usev4`, then `use=foo` is equivalent to
+ `usev4=foov4`.
+ * If both `usev4` and `usev6` are set, then `use=foo` is ignored.
+
+Why this was rejected: The new design would cause existing
+configurations to trigger surprising, and possibly undesired (e.g.,
+timeouts or update errors), new behavior:
+
+ * Configurations that previously updated only an IPv4 record would
+ also update an IPv6 record.
+ * Similarly, configurations that previously updated only an IPv6
+ record would also update an IPv4 record.
+
+### Replace uses of `'wantip'` with `'wantipv4'`
+
+Rather than support `'wantip'`, `'wantipv4'`, and `'wantipv6'`, just
+replace all `'wantip'` references to `'wantipv4'`.
+
+Why this was rejected: This would break compatibility for users that
+are currently updating IPv6 addresses. (Compatibility would be
+restored once the update protocol callbacks are updated to honor
+`'wantipv6'`.)
+
+### Single `if` setting for both `usev4=if` and `usev6=if`
+
+The proposed design calls for separate `ifv4` and `ifv6` settings. If
+the user sets `usev4=if,usev6=if`, then the user most likely wants to
+use the same interface for both IPv4 and IPv6. Rather than create
+separate `ifv4` and `ifv6` settings, have a single `if` setting used
+for both `usev4` and `usev6`.
+
+Why this was rejected:
+ * Separate `v4` and `v6` settings adds consistency to the
+ configuration.
+ * There are cases where a user will want to use a different
+ interface. In particular, an IPv6 over IPv4 tunnel (e.g.,
+ https://tunnelbroker.net) involves creating a separate interface
+ that is used only for IPv6.
+
+### Separate IPv4 and IPv6 credentials
+
+In order to support providers that use separate credentials for IPv4
+and IPv6 updates, the proposed design allows the user to define the
+same host twice. We could instead add additional options so that the
+user can provide both sets of credentials in a single host definition.
+
+Why this was rejected:
+ * The proposed design is easier to implement, as it does not require
+ any modifications to existing protocol implementations.
+ * The proposed design is less likely to cause problems for users
+ that rely on globals instead of host-specific options. For
+ example, a configuration file like the following might not do what
+ the user expects:
+
+ ```
+ ssl=true, use=if, if=eth0
+
+ protocol=foo
+ login=username-for-ipv4
+ password=password-for-ipv4
+ loginv6=username-for-ipv6
+ passwordv6=password-for-ipv6
+ myhost.example.com
+
+ protocol=bar
+ login=username
+ password=password
+ # This host definition will use loginv6, passwordv6 from above
+ # because the user didn't end each setting with a line
+ # continuation:
+ my-other-host.example.com
+ ```
+
+ * The proposed design provides some bonus functionality:
+ * Users can smoothly transition between different providers by
+ updating both providers simultaneously until the domain
+ registration switches to the new registrar.
+ * Users can take advantage of providers that support multiple A
+ or multiple AAAA records for the same hostname, assuming each
+ record has independent credentials.
diff -Nru ddclient-3.9.1/.envrc ddclient-3.10.0/.envrc
--- ddclient-3.9.1/.envrc 1970-01-01 00:00:00.000000000 +0000
+++ ddclient-3.10.0/.envrc 2022-10-20 18:06:35.000000000 +0000
@@ -0,0 +1,5 @@
+if has lorri; then
+ eval "$(lorri direnv)"
+else
+ use nix
+fi
diff -Nru ddclient-3.9.1/.github/workflows/ci.yml ddclient-3.10.0/.github/workflows/ci.yml
--- ddclient-3.9.1/.github/workflows/ci.yml 1970-01-01 00:00:00.000000000 +0000
+++ ddclient-3.10.0/.github/workflows/ci.yml 2022-10-20 18:06:35.000000000 +0000
@@ -0,0 +1,151 @@
+name: CI
+on:
+ push:
+ pull_request:
+
+jobs:
+ test-debian-like:
+ strategy:
+ matrix:
+ image:
+ - ubuntu:latest
+ - ubuntu:16.04
+ - debian:testing
+ - debian:stable
+ - debian:oldstable
+ runs-on: ubuntu-latest
+ container:
+ image: ${{ matrix.image }}
+ steps:
+ - name: install dependencies
+ run: |
+ apt-get update &&
+ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
+ automake \
+ ca-certificates \
+ git \
+ libhttp-daemon-perl \
+ libhttp-daemon-ssl-perl \
+ libio-socket-inet6-perl \
+ libio-socket-ip-perl \
+ libplack-perl \
+ libtest-mockmodule-perl \
+ libtest-tcp-perl \
+ libtest-warnings-perl \
+ liburi-perl \
+ net-tools \
+ make \
+ ;
+ - uses: actions/checkout@v2
+ - name: autogen
+ run: ./autogen
+ - name: configure
+ run: ./configure
+ - name: check
+ run: make VERBOSE=1 AM_COLOR_TESTS=always check
+ - name: distcheck
+ run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
+ - name: distribution tarball is complete
+ run: ./.github/workflows/scripts/dist-tarball-check
+
+ #test-centos6:
+ # runs-on: ubuntu-latest
+ # container: centos:6
+ # steps:
+ # - uses: actions/checkout@v1
+ # - name: install dependencies
+ # run: |
+ # yum install -y \
+ # automake \
+ # perl-IO-Socket-INET6 \
+ # perl-core \
+ # perl-libwww-perl \
+ # ;
+ # - name: autogen
+ # run: ./autogen
+ # - name: configure
+ # run: ./configure
+ # - name: check
+ # run: make VERBOSE=1 AM_COLOR_TESTS=always check
+ # - name: distcheck
+ # run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
+
+ #test-centos8:
+ # runs-on: ubuntu-latest
+ # container: centos:8
+ # steps:
+ # - uses: actions/checkout@v2
+ # - name: install dependencies
+ # run: |
+ # dnf --refresh --enablerepo=PowerTools install -y \
+ # automake \
+ # make \
+ # perl-HTTP-Daemon \
+ # perl-IO-Socket-INET6 \
+ # perl-Test-Warnings \
+ # perl-core \
+ # ;
+ # - name: autogen
+ # run: ./autogen
+ # - name: configure
+ # run: ./configure
+ # - name: check
+ # run: make VERBOSE=1 AM_COLOR_TESTS=always check
+ # - name: distcheck
+ # run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
+
+ test-fedora:
+ runs-on: ubuntu-latest
+ container: fedora
+ steps:
+ - uses: actions/checkout@v2
+ - name: install dependencies
+ run: |
+ dnf --refresh install -y \
+ automake \
+ findutils \
+ make \
+ perl \
+ perl-HTTP-Daemon \
+ perl-HTTP-Daemon-SSL \
+ perl-IO-Socket-INET6 \
+ perl-Plack \
+ perl-Test-MockModule \
+ perl-Test-TCP \
+ perl-Test-Warnings \
+ net-tools \
+ ;
+ - name: autogen
+ run: ./autogen
+ - name: configure
+ run: ./configure
+ - name: check
+ run: make VERBOSE=1 AM_COLOR_TESTS=always check
+ - name: distcheck
+ run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
+
+ test-redhat-ubi7:
+ runs-on: ubuntu-latest
+ # we use redhats univeral base image which is not available on docker hub
+ # https://catalog.redhat.com/software/containers/ubi7/ubi/5c3592dcd70cc534b3a37814
+ container: registry.access.redhat.com/ubi7/ubi
+ steps:
+ - uses: actions/checkout@v2
+ - name: install dependencies
+ run: |
+ yum install -y \
+ automake \
+ make \
+ perl-HTTP-Daemon \
+ perl-IO-Socket-INET6 \
+ perl-core \
+ iproute \
+ ;
+ - name: autogen
+ run: ./autogen
+ - name: configure
+ run: ./configure
+ - name: check
+ run: make VERBOSE=1 AM_COLOR_TESTS=always check
+ - name: distcheck
+ run: make VERBOSE=1 AM_COLOR_TESTS=always distcheck
diff -Nru ddclient-3.9.1/.github/workflows/scripts/dist-tarball-check ddclient-3.10.0/.github/workflows/scripts/dist-tarball-check
--- ddclient-3.9.1/.github/workflows/scripts/dist-tarball-check 1970-01-01 00:00:00.000000000 +0000
+++ ddclient-3.10.0/.github/workflows/scripts/dist-tarball-check 2022-10-20 18:06:35.000000000 +0000
@@ -0,0 +1,71 @@
+#!/bin/sh
+
+pecho() { printf %s\\n "$*"; }
+log() { pecho "$@"; }
+warning() { log "::warning::$@"; }
+error() { log "::error::$@"; }
+fatal() { error "$@"; exit 1; }
+try() { "$@" || fatal "'$@' failed"; }
+
+# actions/checkout@v2 only makes a clone if Git is v2.18 or later, and this
+# test requires a clone.
+git_ver=$(try dpkg-query -f '${Version}' -W git) || exit 1
+dpkg --compare-versions "${git_ver}" ge '1:2.18~' || {
+ warning "This test requires Git v2.18 or later"
+ exit 0
+}
+
+dist_tarball=$(ls ddclient-*.tar.gz) \
+ || fatal "'make dist' must be run before this test"
+
+tmpdir=$(try mktemp -d) || exit 1
+# newer git versions are particular about file ownership which can be ignored here
+git config --global --add safe.directory /__w/ddclient/ddclient || true
+
+log "Copying contents of Git repository..."
+try git archive --format=tar --prefix=git-repo/ HEAD \
+ | try tar -C "${tmpdir}" -xv || exit 1
+(
+ try cd "${tmpdir}"/git-repo
+ # Delete files checked into Git that shouldn't be in the distribution
+ # tarball.
+ try rm -rf \
+ .envrc \
+ .github \
+ .gitignore \
+ docs/ipv6-design-doc.md \
+ shell.nix \
+ ;
+ # TODO: Delete this next line once support for Automake 1.11 is dropped and
+ # tap-driver.sh is removed from the Git repository. It is deleted here to
+ # avoid a spurious diff.
+ try rm -f build-aux/tap-driver.sh
+) || exit 1
+
+log "Extracting distribution tarball..."
+try tar -C "${tmpdir}" -xvzf "${dist_tarball}"
+try mv "${tmpdir}/${dist_tarball%.tar.gz}" "${tmpdir}"/dist-tarball
+(
+ try cd "${tmpdir}"/dist-tarball
+ # Delete generated files
+ try rm -rf \
+ Makefile.in \
+ aclocal.m4 \
+ build-aux/install-sh \
+ build-aux/missing \
+ build-aux/tap-driver.sh \
+ configure \
+ ;
+) || exit 1
+
+log "Comparing Git repository with distribution tarball..."
+cd "${tmpdir}"
+diff -qNr git-repo dist-tarball >/dev/null || {
+ error "Unexpected diff between the repo and the distribution tarball."
+ error "You may need to add a file to EXTRA_DIST in Makefile.am."
+ error "Diff output:"
+ diff -uNr git-repo dist-tarball \
+ | while IFS= read -r line; do error "${line}"; done
+ exit 1
+}
+log "No difference"
diff -Nru ddclient-3.9.1/.gitignore ddclient-3.10.0/.gitignore
--- ddclient-3.9.1/.gitignore 2020-01-08 09:56:03.000000000 +0000
+++ ddclient-3.10.0/.gitignore 2022-10-20 18:06:35.000000000 +0000
@@ -3,3 +3,20 @@
.svn
.cvsignore
*~
+/Makefile
+/Makefile.in
+/aclocal.m4
+/autom4te.cache/
+/build-aux/install-sh
+/build-aux/missing
+/config.log
+/config.status
+/configure
+/ddclient
+/ddclient-*.tar.gz
+/ddclient.conf
+/t/*.log
+/t/*.trs
+/t/geturl_connectivity.pl
+/t/version.pl
+/test-suite.log
diff -Nru ddclient-3.9.1/m4/ax_compare_version.m4 ddclient-3.10.0/m4/ax_compare_version.m4
--- ddclient-3.9.1/m4/ax_compare_version.m4 1970-01-01 00:00:00.000000000 +0000
+++ ddclient-3.10.0/m4/ax_compare_version.m4 2022-10-20 18:06:35.000000000 +0000
@@ -0,0 +1,177 @@
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_compare_version.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_COMPARE_VERSION(VERSION_A, OP, VERSION_B, [ACTION-IF-TRUE], [ACTION-IF-FALSE])
+#
+# DESCRIPTION
+#
+# This macro compares two version strings. Due to the various number of
+# minor-version numbers that can exist, and the fact that string
+# comparisons are not compatible with numeric comparisons, this is not
+# necessarily trivial to do in a autoconf script. This macro makes doing
+# these comparisons easy.
+#
+# The six basic comparisons are available, as well as checking equality
+# limited to a certain number of minor-version levels.
+#
+# The operator OP determines what type of comparison to do, and can be one
+# of:
+#
+# eq - equal (test A == B)
+# ne - not equal (test A != B)
+# le - less than or equal (test A <= B)
+# ge - greater than or equal (test A >= B)
+# lt - less than (test A < B)
+# gt - greater than (test A > B)
+#
+# Additionally, the eq and ne operator can have a number after it to limit
+# the test to that number of minor versions.
+#
+# eq0 - equal up to the length of the shorter version
+# ne0 - not equal up to the length of the shorter version
+# eqN - equal up to N sub-version levels
+# neN - not equal up to N sub-version levels
+#
+# When the condition is true, shell commands ACTION-IF-TRUE are run,
+# otherwise shell commands ACTION-IF-FALSE are run. The environment
+# variable 'ax_compare_version' is always set to either 'true' or 'false'
+# as well.
+#
+# Examples:
+#
+# AX_COMPARE_VERSION([3.15.7],[lt],[3.15.8])
+# AX_COMPARE_VERSION([3.15],[lt],[3.15.8])
+#
+# would both be true.
+#
+# AX_COMPARE_VERSION([3.15.7],[eq],[3.15.8])
+# AX_COMPARE_VERSION([3.15],[gt],[3.15.8])
+#
+# would both be false.
+#
+# AX_COMPARE_VERSION([3.15.7],[eq2],[3.15.8])
+#
+# would be true because it is only comparing two minor versions.
+#
+# AX_COMPARE_VERSION([3.15.7],[eq0],[3.15])
+#
+# would be true because it is only comparing the lesser number of minor
+# versions of the two values.
+#
+# Note: The characters that separate the version numbers do not matter. An
+# empty string is the same as version 0. OP is evaluated by autoconf, not
+# configure, so must be a string, not a variable.
+#
+# The author would like to acknowledge Guido Draheim whose advice about
+# the m4_case and m4_ifvaln functions make this macro only include the
+# portions necessary to perform the specific comparison specified by the
+# OP argument in the final configure script.
+#
+# LICENSE
+#
+# Copyright (c) 2008 Tim Toolan
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 13
+
+dnl #########################################################################
+AC_DEFUN([AX_COMPARE_VERSION], [
+ AC_REQUIRE([AC_PROG_AWK])
+
+ # Used to indicate true or false condition
+ ax_compare_version=false
+
+ # Convert the two version strings to be compared into a format that
+ # allows a simple string comparison. The end result is that a version
+ # string of the form 1.12.5-r617 will be converted to the form
+ # 0001001200050617. In other words, each number is zero padded to four
+ # digits, and non digits are removed.
+ AS_VAR_PUSHDEF([A],[ax_compare_version_A])
+ A=`echo "$1" | sed -e 's/\([[0-9]]*\)/Z\1Z/g' \
+ -e 's/Z\([[0-9]]\)Z/Z0\1Z/g' \
+ -e 's/Z\([[0-9]][[0-9]]\)Z/Z0\1Z/g' \
+ -e 's/Z\([[0-9]][[0-9]][[0-9]]\)Z/Z0\1Z/g' \
+ -e 's/[[^0-9]]//g'`
+
+ AS_VAR_PUSHDEF([B],[ax_compare_version_B])
+ B=`echo "$3" | sed -e 's/\([[0-9]]*\)/Z\1Z/g' \
+ -e 's/Z\([[0-9]]\)Z/Z0\1Z/g' \
+ -e 's/Z\([[0-9]][[0-9]]\)Z/Z0\1Z/g' \
+ -e 's/Z\([[0-9]][[0-9]][[0-9]]\)Z/Z0\1Z/g' \
+ -e 's/[[^0-9]]//g'`
+
+ dnl # In the case of le, ge, lt, and gt, the strings are sorted as necessary
+ dnl # then the first line is used to determine if the condition is true.
+ dnl # The sed right after the echo is to remove any indented white space.
+ m4_case(m4_tolower($2),
+ [lt],[
+ ax_compare_version=`echo "x$A
+x$B" | sed 's/^ *//' | sort -r | sed "s/x${A}/false/;s/x${B}/true/;1q"`
+ ],
+ [gt],[
+ ax_compare_version=`echo "x$A
+x$B" | sed 's/^ *//' | sort | sed "s/x${A}/false/;s/x${B}/true/;1q"`
+ ],
+ [le],[
+ ax_compare_version=`echo "x$A
+x$B" | sed 's/^ *//' | sort | sed "s/x${A}/true/;s/x${B}/false/;1q"`
+ ],
+ [ge],[
+ ax_compare_version=`echo "x$A
+x$B" | sed 's/^ *//' | sort -r | sed "s/x${A}/true/;s/x${B}/false/;1q"`
+ ],[
+ dnl Split the operator from the subversion count if present.
+ m4_bmatch(m4_substr($2,2),
+ [0],[
+ # A count of zero means use the length of the shorter version.
+ # Determine the number of characters in A and B.
+ ax_compare_version_len_A=`echo "$A" | $AWK '{print(length)}'`
+ ax_compare_version_len_B=`echo "$B" | $AWK '{print(length)}'`
+
+ # Set A to no more than B's length and B to no more than A's length.
+ A=`echo "$A" | sed "s/\(.\{$ax_compare_version_len_B\}\).*/\1/"`
+ B=`echo "$B" | sed "s/\(.\{$ax_compare_version_len_A\}\).*/\1/"`
+ ],
+ [[0-9]+],[
+ # A count greater than zero means use only that many subversions
+ A=`echo "$A" | sed "s/\(\([[0-9]]\{4\}\)\{m4_substr($2,2)\}\).*/\1/"`
+ B=`echo "$B" | sed "s/\(\([[0-9]]\{4\}\)\{m4_substr($2,2)\}\).*/\1/"`
+ ],
+ [.+],[
+ AC_WARNING(
+ [invalid OP numeric parameter: $2])
+ ],[])
+
+ # Pad zeros at end of numbers to make same length.
+ ax_compare_version_tmp_A="$A`echo $B | sed 's/./0/g'`"
+ B="$B`echo $A | sed 's/./0/g'`"
+ A="$ax_compare_version_tmp_A"
+
+ # Check for equality or inequality as necessary.
+ m4_case(m4_tolower(m4_substr($2,0,2)),
+ [eq],[
+ test "x$A" = "x$B" && ax_compare_version=true
+ ],
+ [ne],[
+ test "x$A" != "x$B" && ax_compare_version=true
+ ],[
+ AC_WARNING([invalid OP parameter: $2])
+ ])
+ ])
+
+ AS_VAR_POPDEF([A])dnl
+ AS_VAR_POPDEF([B])dnl
+
+ dnl # Execute ACTION-IF-TRUE / ACTION-IF-FALSE.
+ if test "$ax_compare_version" = "true" ; then
+ m4_ifvaln([$4],[$4],[:])dnl
+ m4_ifvaln([$5],[else $5])dnl
+ fi
+]) dnl AX_COMPARE_VERSION
diff -Nru ddclient-3.9.1/m4/ax_prog_perl_modules.m4 ddclient-3.10.0/m4/ax_prog_perl_modules.m4
--- ddclient-3.9.1/m4/ax_prog_perl_modules.m4 1970-01-01 00:00:00.000000000 +0000
+++ ddclient-3.10.0/m4/ax_prog_perl_modules.m4 2022-10-20 18:06:35.000000000 +0000
@@ -0,0 +1,77 @@
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_prog_perl_modules.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_PROG_PERL_MODULES([MODULES], [ACTION-IF-TRUE], [ACTION-IF-FALSE])
+#
+# DESCRIPTION
+#
+# Checks to see if the given perl modules are available. If true the shell
+# commands in ACTION-IF-TRUE are executed. If not the shell commands in
+# ACTION-IF-FALSE are run. Note if $PERL is not set (for example by
+# calling AC_CHECK_PROG, or AC_PATH_PROG), AC_CHECK_PROG(PERL, perl, perl)
+# will be run.
+#
+# MODULES is a space separated list of module names. To check for a
+# minimum version of a module, append the version number to the module
+# name, separated by an equals sign.
+#
+# Example:
+#
+# AX_PROG_PERL_MODULES( Text::Wrap Net::LDAP=1.0.3, ,
+# AC_MSG_WARN(Need some Perl modules)
+#
+# LICENSE
+#
+# Copyright (c) 2009 Dean Povey
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 8
+
+AU_ALIAS([AC_PROG_PERL_MODULES], [AX_PROG_PERL_MODULES])
+AC_DEFUN([AX_PROG_PERL_MODULES],[dnl
+
+m4_define([ax_perl_modules])
+m4_foreach([ax_perl_module], m4_split(m4_normalize([$1])),
+ [
+ m4_append([ax_perl_modules],
+ [']m4_bpatsubst(ax_perl_module,=,[ ])[' ])
+ ])
+
+# Make sure we have perl
+if test -z "$PERL"; then
+AC_CHECK_PROG(PERL,perl,perl)
+fi
+
+if test "x$PERL" != x; then
+ ax_perl_modules_failed=0
+ for ax_perl_module in ax_perl_modules; do
+ AC_MSG_CHECKING(for perl module $ax_perl_module)
+
+ # Would be nice to log result here, but can't rely on autoconf internals
+ $PERL -e "use $ax_perl_module; exit" > /dev/null 2>&1
+ if test $? -ne 0; then
+ AC_MSG_RESULT(no);
+ ax_perl_modules_failed=1
+ else
+ AC_MSG_RESULT(ok);
+ fi
+ done
+
+ # Run optional shell commands
+ if test "$ax_perl_modules_failed" = 0; then
+ :
+ $2
+ else
+ :
+ $3
+ fi
+else
+ AC_MSG_WARN(could not find perl)
+fi])dnl
diff -Nru ddclient-3.9.1/m4/ax_prog_perl_version.m4 ddclient-3.10.0/m4/ax_prog_perl_version.m4
--- ddclient-3.9.1/m4/ax_prog_perl_version.m4 1970-01-01 00:00:00.000000000 +0000
+++ ddclient-3.10.0/m4/ax_prog_perl_version.m4 2022-10-20 18:06:35.000000000 +0000
@@ -0,0 +1,70 @@
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_prog_perl_version.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_PROG_PERL_VERSION([VERSION],[ACTION-IF-TRUE],[ACTION-IF-FALSE])
+#
+# DESCRIPTION
+#
+# Makes sure that perl supports the version indicated. If true the shell
+# commands in ACTION-IF-TRUE are executed. If not the shell commands in
+# ACTION-IF-FALSE are run. Note if $PERL is not set (for example by
+# running AC_CHECK_PROG or AC_PATH_PROG) the macro will fail.
+#
+# Example:
+#
+# AC_PATH_PROG([PERL],[perl])
+# AX_PROG_PERL_VERSION([5.8.0],[ ... ],[ ... ])
+#
+# This will check to make sure that the perl you have supports at least
+# version 5.8.0.
+#
+# NOTE: This macro uses the $PERL variable to perform the check.
+# AX_WITH_PERL can be used to set that variable prior to running this
+# macro. The $PERL_VERSION variable will be valorized with the detected
+# version.
+#
+# LICENSE
+#
+# Copyright (c) 2009 Francesco Salvestrini
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 13
+
+AC_DEFUN([AX_PROG_PERL_VERSION],[
+ AC_REQUIRE([AC_PROG_SED])
+ AC_REQUIRE([AC_PROG_GREP])
+
+ AS_IF([test -n "$PERL"],[
+ ax_perl_version="$1"
+
+ AC_MSG_CHECKING([for perl version])
+ changequote(<<,>>)
+ perl_version=`$PERL --version 2>&1 \
+ | $SED -n -e '/This is perl/b inspect
+b
+: inspect
+s/.* (\{0,1\}v\([0-9]*\.[0-9]*\.[0-9]*\))\{0,1\} .*/\1/;p'`
+ changequote([,])
+ AC_MSG_RESULT($perl_version)
+
+ AC_SUBST([PERL_VERSION],[$perl_version])
+
+ AX_COMPARE_VERSION([$ax_perl_version],[le],[$perl_version],[
+ :
+ $2
+ ],[
+ :
+ $3
+ ])
+ ],[
+ AC_MSG_WARN([could not find the perl interpreter])
+ $3
+ ])
+])
diff -Nru ddclient-3.9.1/m4/ax_with_prog.m4 ddclient-3.10.0/m4/ax_with_prog.m4
--- ddclient-3.9.1/m4/ax_with_prog.m4 1970-01-01 00:00:00.000000000 +0000
+++ ddclient-3.10.0/m4/ax_with_prog.m4 2022-10-20 18:06:35.000000000 +0000
@@ -0,0 +1,70 @@
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_with_prog.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_WITH_PROG([VARIABLE],[program],[VALUE-IF-NOT-FOUND],[PATH])
+#
+# DESCRIPTION
+#
+# Locates an installed program binary, placing the result in the precious
+# variable VARIABLE. Accepts a present VARIABLE, then --with-program, and
+# failing that searches for program in the given path (which defaults to
+# the system path). If program is found, VARIABLE is set to the full path
+# of the binary; if it is not found VARIABLE is set to VALUE-IF-NOT-FOUND
+# if provided, unchanged otherwise.
+#
+# A typical example could be the following one:
+#
+# AX_WITH_PROG(PERL,perl)
+#
+# NOTE: This macro is based upon the original AX_WITH_PYTHON macro from
+# Dustin J. Mitchell .
+#
+# LICENSE
+#
+# Copyright (c) 2008 Francesco Salvestrini
+# Copyright (c) 2008 Dustin J. Mitchell
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 17
+
+AC_DEFUN([AX_WITH_PROG],[
+ AC_PREREQ([2.61])
+
+ pushdef([VARIABLE],$1)
+ pushdef([EXECUTABLE],$2)
+ pushdef([VALUE_IF_NOT_FOUND],$3)
+ pushdef([PATH_PROG],$4)
+
+ AC_ARG_VAR(VARIABLE,Absolute path to EXECUTABLE executable)
+
+ AS_IF(test -z "$VARIABLE",[
+ AC_MSG_CHECKING(whether EXECUTABLE executable path has been provided)
+ AC_ARG_WITH(EXECUTABLE,AS_HELP_STRING([--with-EXECUTABLE=[[[PATH]]]],absolute path to EXECUTABLE executable), [
+ AS_IF([test "$withval" != yes && test "$withval" != no],[
+ VARIABLE="$withval"
+ AC_MSG_RESULT($VARIABLE)
+ ],[
+ VARIABLE=""
+ AC_MSG_RESULT([no])
+ AS_IF([test "$withval" != no], [
+ AC_PATH_PROG([]VARIABLE[],[]EXECUTABLE[],[]VALUE_IF_NOT_FOUND[],[]PATH_PROG[])
+ ])
+ ])
+ ],[
+ AC_MSG_RESULT([no])
+ AC_PATH_PROG([]VARIABLE[],[]EXECUTABLE[],[]VALUE_IF_NOT_FOUND[],[]PATH_PROG[])
+ ])
+ ])
+
+ popdef([PATH_PROG])
+ popdef([VALUE_IF_NOT_FOUND])
+ popdef([EXECUTABLE])
+ popdef([VARIABLE])
+])
diff -Nru ddclient-3.9.1/Makefile.am ddclient-3.10.0/Makefile.am
--- ddclient-3.9.1/Makefile.am 1970-01-01 00:00:00.000000000 +0000
+++ ddclient-3.10.0/Makefile.am 2022-10-20 18:06:35.000000000 +0000
@@ -0,0 +1,155 @@
+ACLOCAL_AMFLAGS = -I m4
+EXTRA_DIST = \
+ CONTRIBUTING.md \
+ COPYING \
+ COPYRIGHT \
+ ChangeLog.md \
+ README.cisco \
+ README.md \
+ README.ssl \
+ autogen \
+ sample-ddclient-wrapper.sh \
+ sample-etc_cron.d_ddclient \
+ sample-etc_dhclient-exit-hooks \
+ sample-etc_dhcpc_dhcpcd-eth0.exe \
+ sample-etc_ppp_ip-up.local \
+ sample-etc_rc.d_ddclient.freebsd \
+ sample-etc_rc.d_init.d_ddclient \
+ sample-etc_rc.d_init.d_ddclient.alpine \
+ sample-etc_rc.d_init.d_ddclient.lsb \
+ sample-etc_rc.d_init.d_ddclient.redhat \
+ sample-etc_rc.d_init.d_ddclient.ubuntu \
+ sample-etc_systemd.service \
+ sample-get-ip-from-fritzbox
+CLEANFILES =
+
+# Command that replaces substitution variables with their values.
+subst = sed \
+ -e 's|@PACKAGE_VERSION[@]|$(PACKAGE_VERSION)|g' \
+ -e '1 s|^\#\!.*perl$$|\#\!$(PERL)|g' \
+ -e 's|@localstatedir[@]|$(localstatedir)|g' \
+ -e 's|@runstatedir[@]|$(runstatedir)|g' \
+ -e 's|@sysconfdir[@]|$(sysconfdir)|g' \
+ -e 's|@CURL[@]|$(CURL)|g'
+
+# Files that will be generated by passing their *.in file through
+# $(subst).
+subst_files = ddclient ddclient.conf
+
+EXTRA_DIST += $(subst_files:=.in)
+CLEANFILES += $(subst_files)
+
+$(subst_files): Makefile
+ rm -f '$@' '$@'.tmp
+ in='$@'.in; \
+ test -f "$${in}" || in='$(srcdir)/'$${in}; \
+ $(subst) "$${in}" >'$@'.tmp && \
+ { ! test -x "$${in}" || chmod +x '$@'.tmp; }
+ mv '$@'.tmp '$@'
+
+ddclient: $(srcdir)/ddclient.in
+ddclient.conf: $(srcdir)/ddclient.conf.in
+
+bin_SCRIPTS = ddclient
+
+sysconf_DATA = ddclient.conf
+
+install-data-local:
+ $(MKDIR_P) '$(DESTDIR)$(localstatedir)'/cache/ddclient
+
+AM_TESTS_ENVIRONMENT = \
+ abs_top_srcdir='$(abs_top_srcdir)'; export abs_top_srcdir;
+LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) \
+ $(top_srcdir)/build-aux/tap-driver.sh
+TEST_EXTENSIONS = .pl
+PL_LOG_DRIVER = $(LOG_DRIVER)
+PL_LOG_COMPILER = $(PERL)
+AM_PL_LOG_FLAGS = -Mstrict -w \
+ -I'$(abs_top_builddir)' \
+ -I'$(abs_top_srcdir)'/t/lib \
+ -MDevel::Autoflush
+handwritten_tests = \
+ t/get_ip_from_if.pl \
+ t/geturl_ssl.pl \
+ t/is-and-extract-ipv4.pl \
+ t/is-and-extract-ipv6.pl \
+ t/is-and-extract-ipv6-global.pl \
+ t/parse_assignments.pl \
+ t/write_cache.pl
+generated_tests = \
+ t/geturl_connectivity.pl \
+ t/version.pl
+TESTS = $(handwritten_tests) $(generated_tests)
+EXTRA_DIST += $(handwritten_tests) \
+ t/lib/Devel/Autoflush.pm \
+ t/lib/Test/Builder.pm \
+ t/lib/Test/Builder/Formatter.pm \
+ t/lib/Test/Builder/IO/Scalar.pm \
+ t/lib/Test/Builder/Module.pm \
+ t/lib/Test/Builder/Tester.pm \
+ t/lib/Test/Builder/Tester/Color.pm \
+ t/lib/Test/Builder/TodoDiag.pm \
+ t/lib/Test/More.pm \
+ t/lib/Test/Simple.pm \
+ t/lib/Test/Tester.pm \
+ t/lib/Test/Tester/Capture.pm \
+ t/lib/Test/Tester/CaptureRunner.pm \
+ t/lib/Test/Tester/Delegate.pm \
+ t/lib/Test/use/ok.pm \
+ t/lib/Test2.pm \
+ t/lib/Test2/API.pm \
+ t/lib/Test2/API/Breakage.pm \
+ t/lib/Test2/API/Context.pm \
+ t/lib/Test2/API/Instance.pm \
+ t/lib/Test2/API/Stack.pm \
+ t/lib/Test2/Event.pm \
+ t/lib/Test2/Event/Bail.pm \
+ t/lib/Test2/Event/Diag.pm \
+ t/lib/Test2/Event/Encoding.pm \
+ t/lib/Test2/Event/Exception.pm \
+ t/lib/Test2/Event/Fail.pm \
+ t/lib/Test2/Event/Generic.pm \
+ t/lib/Test2/Event/Note.pm \
+ t/lib/Test2/Event/Ok.pm \
+ t/lib/Test2/Event/Pass.pm \
+ t/lib/Test2/Event/Plan.pm \
+ t/lib/Test2/Event/Skip.pm \
+ t/lib/Test2/Event/Subtest.pm \
+ t/lib/Test2/Event/TAP/Version.pm \
+ t/lib/Test2/Event/V2.pm \
+ t/lib/Test2/Event/Waiting.pm \
+ t/lib/Test2/EventFacet.pm \
+ t/lib/Test2/EventFacet/About.pm \
+ t/lib/Test2/EventFacet/Amnesty.pm \
+ t/lib/Test2/EventFacet/Assert.pm \
+ t/lib/Test2/EventFacet/Control.pm \
+ t/lib/Test2/EventFacet/Error.pm \
+ t/lib/Test2/EventFacet/Hub.pm \
+ t/lib/Test2/EventFacet/Info.pm \
+ t/lib/Test2/EventFacet/Info/Table.pm \
+ t/lib/Test2/EventFacet/Meta.pm \
+ t/lib/Test2/EventFacet/Parent.pm \
+ t/lib/Test2/EventFacet/Plan.pm \
+ t/lib/Test2/EventFacet/Render.pm \
+ t/lib/Test2/EventFacet/Trace.pm \
+ t/lib/Test2/Formatter.pm \
+ t/lib/Test2/Formatter/TAP.pm \
+ t/lib/Test2/Hub.pm \
+ t/lib/Test2/Hub/Interceptor.pm \
+ t/lib/Test2/Hub/Interceptor/Terminator.pm \
+ t/lib/Test2/Hub/Subtest.pm \
+ t/lib/Test2/IPC.pm \
+ t/lib/Test2/IPC/Driver.pm \
+ t/lib/Test2/IPC/Driver/Files.pm \
+ t/lib/Test2/Tools/Tiny.pm \
+ t/lib/Test2/Util.pm \
+ t/lib/Test2/Util/ExternalMeta.pm \
+ t/lib/Test2/Util/Facets2Legacy.pm \
+ t/lib/Test2/Util/HashBase.pm \
+ t/lib/Test2/Util/Trace.pm \
+ t/lib/ddclient/Test/Fake/HTTPD.pm \
+ t/lib/ddclient/Test/Fake/HTTPD/dummy-ca-cert.pem \
+ t/lib/ddclient/Test/Fake/HTTPD/dummy-server-cert.pem \
+ t/lib/ddclient/Test/Fake/HTTPD/dummy-server-key.pem \
+ t/lib/ddclient/t.pm \
+ t/lib/ok.pm
diff -Nru ddclient-3.9.1/README.md ddclient-3.10.0/README.md
--- ddclient-3.9.1/README.md 2020-01-08 09:56:03.000000000 +0000
+++ ddclient-3.10.0/README.md 2022-10-20 18:06:35.000000000 +0000
@@ -1,29 +1,25 @@
-===============================================================================
-# DDCLIENT v3.9.1
+# DDCLIENT v3.10.0
-ddclient is a Perl client used to update dynamic DNS entries for accounts
+`ddclient` is a Perl client used to update dynamic DNS entries for accounts
on many dynamic DNS services.
-===============================================================================
+## Supported services
Dynamic DNS services currently supported include:
DynDNS.com - See http://www.dyndns.com for details on obtaining a free account.
- Hammernode - See http://www.hn.org for details on obtaining a free account.
Zoneedit - See http://www.zoneedit.com for details.
EasyDNS - See http://www.easydns.com for details.
NameCheap - See http://www.namecheap.com for details
- ConCont - See http://www.dydns.za.net for details
- DnsPark - See http://www.dnspark.com for details
DslReports - See http://www.dslreports.com for details
Sitelutions - See http://www.sitelutions.com for details
Loopia - See http://www.loopia.se for details
Noip - See http://www.noip.com/ for details
Freedns - See http://freedns.afraid.org/ for details
ChangeIP - See http://www.changeip.com/ for details
- dtdns - See http://www.dtdns.com/ for details
nsupdate - See nsupdate(1) and ddns-confgen(8) for details
CloudFlare - See https://www.cloudflare.com/ for details
+ GoDaddy - See https://www.godaddy.com/ for details
Google - See http://www.google.com/domains for details
Duckdns - See https://duckdns.org/ for details
Freemyip - See https://freemyip.com for details
@@ -32,123 +28,204 @@
DNS Made Easy - See https://dnsmadeeasy.com/ for details
DonDominio - See https://www.dondominio.com for details
NearlyFreeSpeech.net - See https://www.nearlyfreespeech.net/services/dns for details
+ OVH - See https://www.ovh.com for details
+ ClouDNS - See https://www.cloudns.net
+ dinahosting - See https://dinahosting.com
+ Gandi - See https://gandi.net
+ dnsexit - See https://dnsexit.com/ for details
+ 1984.is - See https://www.1984.is/product/freedns/ for details
+ Njal.la - See https://njal.la/docs/ddns/
-DDclient now supports many of cable/dsl broadband routers.
+`ddclient` now supports many cable and DSL broadband routers.
-Comments, suggestions and requests: use the issues on
- https://github.com/ddclient/ddclient/issues/new
+Comments, suggestions and requests: use the issues on https://github.com/ddclient/ddclient/issues/new
-The code was originally written by Paul Burry and is now hosted and maintained
+The code was originally written by Paul Burry and is now hosted and maintained
through github.com. Please check out http://ddclient.net
--------------------------------------------------------------------------------
-## REQUIREMENTS:
+## REQUIREMENTS
-- one or more accounts from one of the dynamic DNS services
+ * An account from a supported dynamic DNS service provider
+ * Perl v5.10.1 or later
+ * `IO::Socket::SSL` perl library for ssl-support
+ * `JSON::PP` perl library for JSON support
+ * `IO::Socket::INET6` perl library for ipv6-support
+ * Linux, macOS, or any other Unix-ish system
+ * An implementation of `make` (such as [GNU
+ Make](https://www.gnu.org/software/make/))
+ * If you are installing from a clone of the Git repository, you will
+ also need [GNU Autoconf](https://www.gnu.org/software/autoconf/)
+ and [GNU Automake](https://www.gnu.org/software/automake/).
+
+## DOWNLOAD
+
+See https://github.com/ddclient/ddclient/releases
+
+## INSTALLATION
+
+### Distribution Package
+
+
+ |