#!/usr/bin/perl -w
#
# dnsmax.pl v1.0 - A dynamic DNS update client
#
# This program is currently compatible with the dnsmax.com and
# thatip.com DNS services.
#
# Copyright 2004 Algenta Technologies.
#
# 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 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
use strict;
use XML::Simple qw(:strict);
use Digest::MD5 qw(md5_hex);
use LWP::UserAgent;
use HTTP::Request::Common;
use HTTP::Response;
my $ifconfig = '/sbin/ifconfig';
my $protocol = "https";
#
# sub declarations
#
sub printUsage();
sub configure();
sub fetchHosts();
sub chooseHosts();
sub updateIP();
sub readConfig();
sub writeConfig();
sub getUserInput($$);
my $clientName = "dnsmax.pl";
my $clientVersion = "1.0.1";
my $protocolVersion = "2.0";
# *****************************************************************************
# Program execution
# *****************************************************************************
#
# If there are no arguments, show how to use the program.
#
if (@ARGV < 1)
{
printUsage();
exit();
}
#
# Determine the path of the configuration file to use.
# If one is not specified as the second parameter,
# use dnsmax.conf in the working directory.
#
my $confFile = "dnsmax.conf";
if (@ARGV >= 2)
{
$confFile = $ARGV[1];
}
#
# Determine whether the configuration file already exists,
# and read it if it does.
#
my $isNewConfig = 1;
my $config;
if (-e $confFile)
{
$isNewConfig = 0;
# Read the contents of the configuration file.
$config = readConfig();
#print Dumper($config);
}
#
# Execute the operation they specified.
#
if ($ARGV[0] eq "--configure")
{
configure();
}
elsif ($ARGV[0] eq "--fetchhosts")
{
fetchHosts();
}
elsif ($ARGV[0] eq "--choosehosts")
{
chooseHosts();
}
elsif ($ARGV[0] eq "--updateip")
{
updateIP();
}
else
{
printUsage();
}
exit();
# *****************************************************************************
# End program execution, begin functions
# *****************************************************************************
#
# sub printUsage()
# Print usage information to standard output.
#
sub printUsage()
{
print "\n";
print "Usage: dnsmax.pl MODE [configuration_file]\n";
print "\n";
print "Available Modes\n";
print "\n";
print "--configure Create or edit a configuration file; set the account's\n";
print " user name, password, and other settings.\n";
print "--fetchhosts Retrieve the latest list of dynamic DNS records for\n";
print " the configured account. Determine which records\n";
print " should be updated when an IP change is detected.\n";
print "--choosehosts Choose which hosts will be updated when an IP change is\n";
print " detected.\n";
print "--updateip Determine whether the network's IP has changed since the\n";
print " the last time it was checked, and send an update if\n";
print " necessary.\n";
print "\n";
print "If configuration_file is not specified, a file named dnsmax.conf located\n";
print "in the working directory will be assumed.\n";
print "\n";
}
#
# sub configure()
#
#
sub configure()
{
#
# Print some instructions.
#
print "\n";
print "Configuring $confFile\n";
print "\n";
print "Please enter the appropriate settings for the following\n";
print "items. If you see a value between brackets, you can simply\n";
print "press enter to use that value.\n";
print "\n";
#
# Set defaults based on the existing configuration.
#
my $defaultUsername = "";
my $defaultPassword = "";
my $defaultServerHost = "update.dnsmax.com";
my $defaultServerPort = "443";
my $defaultDoGatewayCheck = "no";
my $defaultGatewayCheckPort = "22123";
if (defined($config))
{
$defaultUsername = $config->{Accounts}->{Account}->{Username};
$defaultServerHost = $config->{Accounts}->{Account}->{ServerHost};
$defaultServerPort = $config->{Accounts}->{Account}->{ServerPort};
$defaultDoGatewayCheck =
($config->{DoGatewayCheck} eq "true") ? "yes" : "no";
$defaultGatewayCheckPort = $config->{GatewayCheckPort};
}
#
# Go through all the settings and ask the user
# what they should be. Use any existing settings
# that we have read in as defaults.
#
my $username = getUserInput(
"username @ service (e.g., username\@thatip.com or username\@dnsmax.com",
$defaultUsername);
my $password = getUserInput("Password", $defaultPassword);
my $serverHost = getUserInput("Update server", $defaultServerHost);
my $serverPort = getUserInput("Update server port", $defaultServerPort);
my $doGatewayCheck = getUserInput("Perform gateway check", $defaultDoGatewayCheck);
$doGatewayCheck = (substr(lc($doGatewayCheck), 0, 1) eq "y") ? "yes" : "no";
my $gatewayCheckPort = $defaultGatewayCheckPort;
if ($doGatewayCheck eq "yes")
{
$gatewayCheckPort = getUserInput("Port to use for gateway check", $defaultGatewayCheckPort);
}
#
# Update the variables in our config object.
#
$config->{Accounts}->{Account}->{Username} = $username;
$config->{Accounts}->{Account}->{Md5Password} = md5_hex($password);
$config->{Accounts}->{Account}->{ServerHost} = $serverHost;
$config->{Accounts}->{Account}->{ServerPort} = $serverPort;
$config->{DoGatewayCheck} = ($doGatewayCheck eq "yes") ? "true" : "false";
$config->{GatewayCheckPort} = $gatewayCheckPort;
#
# Write the configuration file.
#
writeConfig();
print "Your configuration has been saved in $confFile.\n";
#
# If this is a new account, fetch the records from the server.
#
if ($isNewConfig == 1)
{
fetchHosts();
chooseHosts();
}
}
#
# sub fetchHosts()
#
#
sub fetchHosts()
{
print "Fetching records from the server...\n";
#
# Make a copy of our current list of records.
# We need this so we remember whether the records
# that are still around should be set to update or not.
#
my @newRecords = ();
my @oldRecords = ();
if(exists $config->{Accounts}->{Account}->{Records}) {
@oldRecords = @{$config->{Accounts}->{Account}->{Records}->{DnsRecord}};
}
# Get rid of the existing records now that we have a backup.
$config->{Accounts}->{Account}->{Records} = ();
#
# Construct the URI of the update server.
#
my $updateServer = $config->{Accounts}->{Account}->{ServerHost};
my $updatePort = $config->{Accounts}->{Account}->{ServerPort};
if (!defined($updateServer) or !defined($updatePort))
{
print "The update server or port could not be determined.\n";
print "Please make sure your configuration file is valid and that\n";
print "you have specified an update server and port.\n";
print "Please try running dnsmax.pl --configure.\n";
die();
}
my $recordsUri = "$protocol://$updateServer:$updatePort/records/";
print "Using $recordsUri\n";
#
# Make our HTTP request for the records.
#
my $ua = LWP::UserAgent->new;
my $response = $ua->request(POST "$recordsUri",
[
username => $config->{Accounts}->{Account}->{Username},
passwordmd5 => $config->{Accounts}->{Account}->{Md5Password},
clientname => $clientName,
clientversion => $clientVersion,
protocolversion => $protocolVersion,
contenttype => "text/plain",
]
);
#
# Parse the response to create our list of current records.
#
if (!$response->is_success)
{
print "There was a problem fetching your records from the server.\n";
print $response->status_line . "\n";
exit();
}
# Split on a colon. We need exactly two fields.
my @lines = split(/\n/, $response->content);
foreach my $line (@lines)
{
my ($key, $val) = split(/:/, $line);
if ($key eq "serial")
{
$config->{Accounts}->{Account}->{Serial} = $val;
}
elsif($key eq "record")
{
# Split this by tabs. We need at least 3 tokens.
# <recordid> <record name> <record type> <optional group>
my @tokens = split(/\t/, $val);
my $numTokens = @tokens;
if ($numTokens < 4)
{
print "Invalid record. $val\n";
next;
}
# Create a new record and set the properties.
my $record;
$record->{Id} = $tokens[0];
$record->{Host} = $tokens[1];
$record->{Zone} = $tokens[2];
$record->{Type} = $tokens[3];
$record->{Dynamic} = 'true';
$record->{Update} = 'false';
# Set the group if appropriate.
if ($numTokens >= 5)
{
$record->{Group} = $tokens[4];
}
push(@newRecords, $record);
}
elsif($key eq "errorcode")
{
print "Error code: $val\n";
}
elsif($key eq "errortext")
{
print "Error details: $val\n";
}
else
{
print "Unknown response. $key: $val\n";
}
}
#
# Go through the list of current records. If the same record
# existed before this fetch, set whether to update it to
# whatever it was set to previously.
#
foreach my $newRecord (@newRecords)
{
# Loop through the old records to see if any match this one.
foreach my $oldRecord (@oldRecords)
{
if ($newRecord->{Id} == $oldRecord->{Id} and
$newRecord->{Host} eq $oldRecord->{Host} and
$newRecord->{Zone} eq $oldRecord->{Zone} and
$newRecord->{Type} eq $oldRecord->{Type})
{
$newRecord->{Update} = $oldRecord->{Update};
}
}
}
#
# Save the updated configuration.
#
$config->{Accounts}->{Account}->{Records}->{DnsRecord} = \@newRecords;
writeConfig();
}
#
# sub chooseHosts()
#
#
sub chooseHosts()
{
#
# Display a list of hosts and whether they are set to be updated.
#
print "\n";
print "Dynamic DNS records\n";
print "\n";
printRecordList();
#
# Prompt for input. Input should either be a hostname to toggle
# or the word "list" or "done" or "all" or "none".
#
print "\n";
print "To toggle updates, type the full record or group name and press enter.\n";
print "Type 'all' to set all records to update, or 'none' to set none to update.\n";
print "Type 'done' when you are finished.\n";
my $input = '';
while ($input ne 'done')
{
$input = getUserInput('?', '');
if ($input eq "done")
{
last;
}
elsif ($input eq "list")
{
printRecordList();
print "\n";
}
# Go through each record and set it as appropriate.
my @records = @{$config->{Accounts}->{Account}->{Records}->{DnsRecord}};
foreach my $record (@records)
{
if ($input eq "all") {
$record->{Update} = "true";
}
elsif ($input eq "none") {
$record->{Update} = "false";
}
elsif (defined($record->{Group}) and $input eq $record->{Group}) {
$record->{Update} = $record->{Update} eq "true" ? "false" : "true";
}
else {
if ($record->{Host} eq "@") {
if ($input eq $record->{Zone}) {
$record->{Update} = $record->{Update} eq "true" ? "false" : "true";
}
}
else {
if ($input eq $record->{Host} . '.' . $record->{Zone}) {
$record->{Update} = $record->{Update} eq "true" ? "false" : "true";
}
}
}
}
}
print "\n";
#
# Write the configuration file with the updated information.
#
writeConfig();
}
#
# sub printRecordList()
# Print a list of the records and groups.
#
sub printRecordList()
{
my @records = @{$config->{Accounts}->{Account}->{Records}->{DnsRecord}};
my %listedGroups = ();
foreach my $record (@records)
{
my $showRecord = 1;
# If this record is in a group, see if we have already listed the group name.
# If we haven't, then list it and remember that fact.
if (defined($record->{Group}))
{
if (!defined($listedGroups{$record->{Group}}))
{
print $record->{Group} . " (Group)";
$listedGroups{$record->{Group}} = 1;
}
else
{
$showRecord = 0;
}
}
else
{
my $fullName = "";
if ($record->{Host} eq "@")
{
$fullName = $record->{Zone};
}
else
{
$fullName = $record->{Host} . "." . $record->{Zone};
}
print $fullName;
}
if ($showRecord == 1)
{
if ($record->{Update} eq 'true')
{
print " (Updates on)";
}
else
{
print " (Updates off)";
}
print "\n";
}
}
print "\n";
}
#
# sub updateIP()
#
#
sub updateIP()
{
#
# Go through the list of hosts and determine which ones
# need to be updated. If no hosts need to be updated,
# then we don't need to do this.
#
my $hasRecordToUpdate = 0;
my %addedGroups = ();
my @updateIds = ();
my @updateGroups = ();
if(!exists $config->{Accounts}->{Account}->{Records}) {
print "No records are known.\n";
print "You can create records in the management website.\n";
print "Please use dnsmax.pl --fetchhosts to download your records.\n";
return;
}
foreach my $record (@{$config->{Accounts}->{Account}->{Records}->{DnsRecord}})
{
# If this record is marked to be updated
if ($record->{Update} eq "true")
{
# if this is a single record
if (!defined($record->{Group}))
{
$hasRecordToUpdate = 1;
push(@updateIds, $record->{Id});
}
else
{
if (!defined($addedGroups{$record->{Group}}))
{
$hasRecordToUpdate = 1;
push(@updateGroups, $record->{Group});
$addedGroups{$record->{Group}} = 1;
}
}
} # update set to true
} #foreach record
# If there are not any records to update, get out of here.
if ($hasRecordToUpdate != 1)
{
print "None of your records are set to be updated.\n";
print "Please use dnsmax.pl --choosehosts to enable some records.\n";
return;
}
#
# Determine whether the IP has changed since the last time
# this was called. If not, we don't need to send an update.
#
print "Checking for an IP address change...\n";
my $ipChanged = 0;
my $lastIP = $config->{LastIP};
if(!exists $config->{LastIP}) {
$ipChanged = 1;
}
my $ipoutput = `$ifconfig`;
my @iplines = split(/\n/, $ipoutput);
foreach my $line (@iplines)
{
if ($line =~ /addr:\s*(\w+[.:]+\w+[.:]\w+[.:]\w+)/)
{
my $ip = $1;
if ($ip =~/^127./ or
$ip =~ /^10./ or
$ip =~ /^192.168./ or
$ip =~ /^172./ or
$ip =~ /^fe80/)
{
next;
}
if (!defined($lastIP) or $lastIP ne $ip)
{
$ipChanged = 1;
$config->{LastIP} = $ip;
writeConfig();
last;
}
}
}
if ($ipChanged != 1)
{
print "Your IP address is already up to date.\n";
return;
}
print "Updating records...\n";
#
# Build the URI for the update request.
#
my $updateServer = $config->{Accounts}->{Account}->{ServerHost};
my $updatePort = $config->{Accounts}->{Account}->{ServerPort};
if (!defined($updateServer) or !defined($updatePort))
{
print "The update server or port could not be determined.\n";
print "Please make sure your configuration file is valid and that\n";
print "you have specified an update server and port.\n";
print "Please try running dnsmax.pl --configure.\n";
die();
}
my $updateUri = "$protocol://$updateServer:$updatePort/update/";
#
# Make the update request.
#
my $ua = LWP::UserAgent->new;
my $updateRequest = POST "$updateUri",
[
'username' => $config->{Accounts}->{Account}->{Username},
'passwordmd5' => $config->{Accounts}->{Account}->{Md5Password},
'clientname' => $clientName,
'clientversion' => $clientVersion,
'protocolversion' => $protocolVersion,
'contenttype' => "text/plain",
] ;
# Add the appropriate IDs and Groups to the request.
foreach my $updateid (@updateIds)
{
$updateRequest->add_content("&updateid[]=$updateid");
}
foreach my $updategroup (@updateGroups)
{
$updateRequest->add_content("&updategroup[]=$updategroup");
}
# We need to fix the content length, as add_content apparently
# doesn't take care of that.
$updateRequest->header("Content-Length" => length(${$updateRequest->content_ref}));
my $response = $ua->request($updateRequest);
#
# Give some output based on the response.
#
if (!$response->is_success)
{
print "There was a problem updating your hosts.\n";
print $response->status_line . "\n";
exit();
}
my @lines = split(/\n/, $response->content);
foreach my $line (@lines)
{
my ($key, $val) = split(/:/, $line);
if ($key eq "serial")
{
# Compare this serial number to the one we have recorded.
# If they are different, instruct the user to update the
# records list.
if ($val ne $config->{Accounts}->{Account}->{Serial})
{
print "Your local record list appears to be out of date.\n";
print "Please use dnsmax.pl --fetchhosts to update your list.\n";
}
}
elsif ($key eq "remoteip")
{
print "Your records have been updated to point to $val.\n";
# Update our last known IP.
$config->{LastIP} = $val;
writeConfig();
}
elsif($key eq "errorcode")
{
print "Error code: $val\n";
}
elsif($key eq "errortext")
{
print "Error details: $val\n";
}
else
{
print "Unknown response. $key: $val\n";
}
}
}
#
# sub readConfig()
#
#
sub readConfig()
{
#return XMLin($confFile, KeyAttr => { DNSRecord => "Id" },
# ForceArray => ["DNSRecord"] );
return XMLin($confFile, KeyAttr => [],
ForceArray => [qw(DnsRecord )] );
}
#
# sub writeConfig()
#
#
sub writeConfig()
{
XMLout($config, RootName => "AppConfiguration",
XMLDecl => "<?xml version=\"1.0\"?>",
NoAttr => 1,
KeyAttr => { DNSRecord => "Id"},
OutputFile => $confFile);
}
#
# sub getUserInput
# $label: text with which to prompt the user.
# $default: the default value in case the user just presses enter.
#
sub getUserInput($$)
{
my ($label, $default) = @_;
my $input = "";
while ($input eq "")
{
print "$label [$default]: ";
$input = <STDIN>;
chop $input;
if ($input eq "")
{
$input = $default;
}
}
return $input;
}
syntax highlighted by Code2HTML, v. 0.9.1