#!/usr/bin/env perl
# @(#) DnsPixie.pl  Update client for DtDNS, FreeDNS and other popular
#                   dynamic DNS services. Rev'd: 2015-01-25.
#
# Copyright (c) 2014 Graham Jenkins <grahjenk@cpan.org>. All rights reserved.
# This program is free software; you can redistribute it and/or modify it under
# the same terms as Perl itself.

use strict;
use warnings;
use File::Basename;
use vars qw($VERSION);
use LWP::Simple qw($ua get);
use Getopt::Std;
$VERSION=1.05;
$ua->timeout(30);
$ua->agent("DnsPixie.pl/$VERSION");
$Getopt::Std::STANDARD_HELP_VERSION=1;

# Collect options, check usage, open configuration file
my ($Conn,$BadOption,%opts)=("https",,);
getopts ('udm:l:',\%opts) or $BadOption="Y";
if ( ($#ARGV != 0) || defined($BadOption) ||
     (defined($opts{'m'}) && ($opts{'m'} !~ m/^\d+$/)) ) {
  die "Usage: ".basename($0)." [-u] [-d] [-m N] [-l led] config-file\n".
      "Refer: perldoc ".basename($0)."\n"
}
if ( defined($opts{'m'})&&($opts{'m'}==0) ){die "Zero interval not allowed!\n"}
if ( ! open(CF,$ARGV[0]) ) { die "Can't read file: ".$ARGV[0]."\n" }

# Read and store potentially meaningful records, then close the file
my (%Type,%Pass,%OldAddr,%ChangeTime);
while (<CF>) {
  chomp;
  my ($t,$h,$p) = split;
  if ( defined($p) && ($t !~ /^\043/) ) {
    $Type{$h}=$t; $Pass{$h}=$p; $OldAddr{$h}="x"; $ChangeTime{$h}=time()
  }
}
close(CF);
if ( keys(%Pass) < 1 ) { die "No valid records found in file: ".$ARGV[0]."\n" }

# Sources for current IP address; use these in sequence
my @MyIps=("myip.dtdns.com","myip.dnsomatic.com","xpertdns.com/myip.php",
           "ip.dnsexit.com","ip1.dynupdate.no-ip.com","ip2.dnsexit.com");

# Use Syslog if we can
my $rc=eval { require Sys::Syslog; Sys::Syslog->import(); 1 };
if ($rc) { openlog(basename($0),"","user") }
else     { sub syslog {}                   }

# Process 'u' and 'd' options
if ( defined($opts{'u'}) ) { $Conn="http"  }
if ( defined($opts{'d'}) ) { fork and exit }
 
# Loop once if repeat-interval not set, else loop forever
while (1) {
  # Select next IP-address source, get current address
  my $MyIp=shift(@MyIps); push(@MyIps,$MyIp);
  my ($Status,$Address)=(0,);
  if ( defined($Address=get("http://".$MyIp)) &&
                         ($Address=~m/^((\d){1,3}\.){3}(\d){1,3}(\s){0,}$/) ) {
    $Address=~s/\s+//g;
    # Update each host address on first pass or if current address changed;
    # also force update if this program hasn't changed it during last 7 days.
    foreach my $h ( sort(keys(%Pass)) ) {
      if ( ($Address ne $OldAddr{$h}) || (time()-$ChangeTime{$h}>7*24*3600) ) {
        syslog("info","Attempting update for $h to: $Address");
        my ($p,$Response,$Good)=($Pass{$h},,);
        if ( $Type{$h} eq "DtDNS"   ) {
          if ( defined($Response=get(
              "$Conn://www.dtdns.com/api/autodns.cfm?id=$h&pw=$p")) ) {
            if ( $Response=~m/now points/ ) { $Good="Y" }
          }
        }
        if ( $Type{$h} eq "FreeDNS" ) {
          if ( defined($Response=get(
              "$Conn://freedns.afraid.org/dynamic/update.php?".$p)) ) {
            if ( ($Response=~m/has not chang/) || ($Response=~m/^Updated/) )  {
              $Response=~s/^ERROR: //; $Good="Y"
            }
          } 
        }
        if ( $Type{$h} eq "DuckDNS" ) {
          my $x=lc($h); $x=~s/\.duckdns\.org//;
          if ( defined($Response=get(
              "$Conn://www.duckdns.org/update?domains=$x&token=$p&ip=")) )    {
            if ( $Response eq "OK"  ) { $Good="Y" }
          }
        }
        if ( $Type{$h} eq "ChangeIP" ) {
          my ($x,$y)=split /:/,$p;
          if ( defined($Response=get(
              "$Conn://nic.ChangeIP.com/nic/update?u=$x&p=$y&hostname=$h")) ) {
            if ( $Response=~m/200 Successful Update/ ) { $Good="Y" }
          }
        }
        if ( defined($Good) ) { 
          $OldAddr{$h}=$Address; $ChangeTime{$h}=time();
          syslog("info"," ==> $Response")
        }
        else { $Status=1; syslog("info","Update failed!") }
      }
    }
  }
  else { $Status=1;syslog("info","Can't get current address from: $MyIp")}
  if ( defined($opts{'m'}) ) {
    if ( defined($opts{'l'}) && ($Status==0) && 
         blink($opts{'l'},60*$opts{'m'}) ) {} else {sleep(60*$opts{'m'})}
  }
  else                       { exit($Status)      }
}

sub blink { # Usage: blink(led,secs) .. blinks 'led' for 'secs' seconds
  my $led=shift; my $secs=shift;
  if ( open(TRIGGER,">/sys/class/leds/$led/trigger") ) {
    print TRIGGER "none\n"; close(TRIGGER); 
    my $bright=1;
    if ( open(MAX,"</sys/class/leds/$led/max_brightness") ) {
      while (<MAX>) { chomp; if ( $_=~/^\d+$/) {$bright=$_} }
      close(MAX)
    }
    for (my $k=0;$k<$secs;$k++) {
      open(BRIGHT, ">/sys/class/leds/led0/brightness");
      print BRIGHT ($k%2)*$bright,"\n"; sleep(1); close(BRIGHT)
    }
    return(1)
  }
  return(0)
}

__END__

=head1 NAME

DnsPixie - update client for popular dynamic DNS services

=head1 README

DnsPixie will update one or more designated DNS records at
www.dtdns.com, freedns.afraid.org and elsewhere either once or periodically.

=head1 DESCRIPTION

C<DnsPixie> is a simple update client for the DtDNS, FreeDNS and other dynamic
DNS services. It will attempt an immediate update when called,
then optionally loop at designated intervals, attempting 
further updates when necessary.

=head1 USAGE

=over 6

DnsPixie [-u] [-d] [-m repeat-interval] [-l led] config-file

=back

e.g.: DnsPixie -m 15 /usr/local/etc/DnsPixie.conf

The repeat-interval value (where present) must be
expressed as a positive integer number of minutes.
If the '-u' flag is included, then an unsecure (http)
connection will be used.

The config-file value must be the name of a configuration
file which contains one or more 'service hostname password' records
where each password relates to the owner of the
corresponding hostname. Each record should appear on a
separate line thus:

  DtDNS lassie.dtdns.net     mypassword
  FreeDNS daisy.moo.com      ABC7PqRsTUvwXYEjLLM2R7ST8uvWX92AA3BbCz
  DuckDNS donald.duckdns.org 064a0540-864c-4f0f-8bf5-23857452b0c1
  ChangeIP fido.changeip.net myuser:mypassword
   .. etc.

Lines which begin in "#" will be ignored.
It is suggested that the configuration file should be
readable only by the intended program-user.
I<Sys::Syslog> will be used for logging if it's installed;
you might want to check your system's syslog configuration so
that you can view log messages.

To start DnsPixie on a Linux or BSD machine, you can include an
appropriate command in an rc.local file, using the '-d' flag so that
DnsPixie will daemonize itself. On a Windows machine, you can start
DnsPixie as a scheduled task.

On Linux machines which have a system LED, you can include the '-l'
flag with a name like 'led0' (for the Raspberry Pi); DnsPixie will
then attempt to blink the designated led during successful execution.

=head1 NOTE

By default DnsPixie uses a secure (https) connection. For this to
work, you need to ensure that I<LWP::Protocol::https> is installed.

=head1 SCRIPT CATEGORIES

Networking
UNIX/System_administration

=head1 AUTHOR

Graham Jenkins <grahjenk@cpan.org>

=head1 COPYRIGHT

Copyright (c) 2014 Graham Jenkins. All rights reserved.
This program is free software; you can redistribute it
and/or modify it under the same terms as Perl itself.

=cut
