#!/usr/bin/perl # # eponym.pl # Eponym is a free DynDNS & ZoneEdit auto-updater. # # This program is the copyrighted work of Encodable Industries. # Redistribution is prohibited, and copying is permitted only # for backup purposes. You are free to modify the program for # your own use, but you may not distribute any modified copies # of it. # # This software comes with no warranty. The author and many other # people have found it to be useful, and it is our hope that you # find it useful as well, but it comes with no guarantees. Under # no circumstances shall Encodable Industries be held liable in # any situation arising from your use of this program. We are # generally happy to provide support to all our users, but we can # make no guarantee of support. # # For more information about this program, visit the following pages: # # Homepage: http://encodable.com/eponym/ # For Help: http://encodable.com/contact/ my $version = "2.72"; use strict; use POSIX; for('LWP::UserAgent', 'IO::Socket', 'MIME::Base64', 'MIME::Lite', 'Term::ReadKey', 'Net::DNS') { eval "require $_"; if($@) { print qq`\nThe $_ module is required, but could not be found.` . qq`\nTo install it, open a command prompt and run the command "ppm"` . qq`\n(for Windows users) or "perl -MCPAN -e shell" (for Linux users).` . qq`\nThen you'll have either a "ppm>" prompt or a "cpan>" prompt.`; if(/MIME::Lite/) { print qq`\nWindows users, type:\n\n\tinstall MIME-Lite`; print qq`\n\nLinux users, type:\n\n\tinstall MIME::Lite`; } elsif(/Term::ReadKey/) { print qq`\nWindows users, type:\n\n\tinstall TermReadKey`; print qq`\n\nLinux users, type:\n\n\tinstall Term::ReadKey`; } elsif(/Net::DNS/) { print qq`\nWindows users, type:\n\n\tinstall Net-DNS`; print qq`\n\nLinux users, type:\n\n\tinstall Net::DNS`; } else { print qq`\nType:\n\n\tinstall $_`; } print qq`\n\n(Note: in case it can't find the module, use the search command` . qq`\nto search for part of the module name.)` . qq`\n` . qq`\nWhen it finishes, type "exit" and then run Eponym again.` . qq`\n` . qq`\n(You can also install modules non-interactively using the command` . qq`\n"ppm install ModuleName" on Windows, or` . qq`\n"perl -MCPAN -e 'install ModuleName'" on Linux.)` . qq`\n` . qq`\n[ For more help, see http://encodable.com/perl_modules/ ]` . qq`\n[ Or contact us at http://encodable.com/contact/ ]` . qq`\n`; exit; } } my (@hosts_to_update, %ip_reporters, %PREF, $extra_code, $running_mswindows) = (); my $scriptname = 'eponym'; my $curdir; if($^O =~ /MSWin32/) { $curdir = './'; $running_mswindows = 1; } else { $curdir = `dirname "$0"`; chomp $curdir; $curdir = $curdir . '/'; } my $prefs_file = $curdir . $scriptname . '_prefs.txt'; if( (! -e $prefs_file) && ($running_mswindows) ) { # try manually setting the current directory to c:\windows since that's where we install by default. $curdir = 'c:/windows/'; $prefs_file = $curdir . $scriptname . '_prefs.txt'; die "Error: could not find prefs file '$prefs_file'.\n" unless -e $prefs_file; } my $pid_file = $curdir . $scriptname . '-current-pid.txt'; my $state_file = $curdir . $scriptname . '-current-state.txt'; my $data_file = $curdir . $scriptname . '-current-data.txt'; load_prefs(); verify_prefs(); write_pid_file($pid_file); clear_file($state_file); my $here = $scriptname . '.pl'; my $error_log = $curdir . $scriptname . '_errors.log'; my $http_ua = "eponym/$version support\@encodable.com"; my ($header_dyndns, $header_zoneedit) = (); if($PREF{'dyndns_authorization'} =~ /\w/) { $header_dyndns = HTTP::Headers->new(user_agent => $http_ua); $header_dyndns->authorization("Basic $PREF{'dyndns_authorization'}"); } if($PREF{'zoneedit_authorization'} =~ /\w/) { $header_zoneedit = HTTP::Headers->new(user_agent => $http_ua); $header_zoneedit->authorization("Basic $PREF{'zoneedit_authorization'}"); } my %options = (); for(@ARGV) { $options{$_} = 1; } # We only need one instance of the agent, so it's up here, before the loop. my $agent = LWP::UserAgent->new(); my $inettimeout = 10; # Sometimes when attempting to detect your IP address, we can't # do it. This might be because you are actually offline, or it # might be because one of the websites that reports your IP address # is down. In any case, we'll try to detect it max_retries times # before going back to sleep for detection_frequency minutes. my $max_retries = 10; if($PREF{'detection_frequency'} !~ /^\d+$/) { $PREF{'detection_frequency'} = 30; } elsif($PREF{'detection_frequency'} < 5) { $PREF{'detection_frequency'} = 5; } my $sleeptime = $PREF{'detection_frequency'} * 60; my $sleeptime_default = $sleeptime; my $num_reporters = scalar keys %ip_reporters; my $wildcard_option; if($PREF{'wildcard'} =~ /yes/i) { $wildcard_option = '&wildcard=ON'; } if($ARGV[0] eq '--getauth') { $| = 1; my $secret = $options{'--notsecret'} ? 0 : 1; print "Please type your DynDNS.org or ZoneEdit.com account's username: "; my $user = $secret ? Term::ReadKey::ReadLine(0) : ; chomp $user; my ($pw1, $pw2) = (1,2); while($pw1 ne $pw2) { Term::ReadKey::ReadMode('noecho') if $secret; print "Please type the password for that account: "; $pw1 = $secret ? Term::ReadKey::ReadLine(0) : ; print "\nPlease type the password again to verify: "; $pw2 = $secret ? Term::ReadKey::ReadLine(0) : ; Term::ReadKey::ReadMode(0) if $secret; chomp ($pw1, $pw2); print "\n\nERROR: passwords do not match. Please try again.\n\n" unless $pw1 eq $pw2; } print "\n\nYour authorization string is:\n\n" . MIME::Base64::encode("$user:$pw1", '' ) . "\n"; print "\nNow edit your ${scriptname}_prefs.txt file and put that string at the end of"; print "\nthe line that says either:"; print "\n\n\tdyndns_authorization = "; print "\n\n...or:"; print "\n\n\tzoneedit_authorization = "; print "\n\n...depending on which username/password you entered.\n"; exit; } if(($num_reporters < 8) && !$PREF{ip_detection_command}) { die qq` Not enough IP address reporters. You need to search the web for the phrase "your ip address is" (in quotes) and find at least 8 websites that report your IP address. There are thousands of sites that do. Since this script will be checking them frequently, each user must find their own sites, so that we don't overload a certain small set of servers. Once you get 8 of them, then edit the $prefs_file file and add those websites as ip_reporter values. Alternatively, you can set the ip_detection_command instead of using the ip_reporter feature, but that won't work on some systems. `; } if($hosts_to_update[0] !~ /\w+:\w+/ && $PREF{hostless_mode} !~ /yes/i) { die " You need to edit the $prefs_file file and enter the hostname(s) that you wish to update. "; } if($#hosts_to_update == -1 && $PREF{hostless_mode} !~ /yes/i) { die "You haven't specified any hosts to update.\n"; } if($PREF{hostless_mode} !~ /yes/i) { for(@hosts_to_update) { if( (/^zoneedit:/ && $PREF{'zoneedit_authorization'} !~ /\w/) || (/^dyndns:/ && $PREF{'dyndns_authorization'} !~ /\w/) ) { die "\nYou need to edit the $prefs_file file and set your authorization" . "\nstring. To determine your authorization string, call this script" . "\nlike this:" . "\n\n\tperl $here --getauth" . "\n\n...or if that gives you problems (over OpenSSHWin it does), use this:" . "\n\n\tperl $here --getauth --notsecret" . "\n\n"; } } } print_func(qq` ---------------------------------------------------------------------------- Eponym: updates a dynamic DNS host with your non-static home IP address. Detects your IP address every-so-often by visiting websites that report your IP to you. Actually, we visit 3 such sites from a pool of 10 or 15, and consider the detection "successful" if we get 3 sites reporting the same IP address for you. We then check what IP your dynamic DNS host currently resolves to, and if that doesn't match your current home IP, then we update the dynamic DNS database. This script can update multiple hosts on the same account, and it supports all the top-level-domain endings available on the DynDNS.org service, not just *.dyndns.org. It also supports zoneedit.com's top-level domain names. The command-line options are: --offline, which is only available to "credited users" of DynDNS.org --force, which will update your hostname(s) even if they don't need it, but only once --forever, which will check if your hostname(s) need updates (and do them if so) and then sleep for a few minutes until the next update check --testmode, which will cause Eponym to do the IP testing, but not actually update any hostnames in the DynDNS/ZoneEdit systems Contact us at http://encodable.com/contact/ if you need any help, and see also http://encodable.com/eponym/ for updates. NOTE: the files eponym-current-pid.txt and -current-state.txt will tell you Eponym's PID and what Eponym is currently doing. ---------------------------------------------------------------------------- `); exit if ($options{-h} || $options{'--h'} || $options{-help} || $options{'--help'}); print qq`Checking your IP address...\n\n`; # Main loop. while(1) { my ($firstline, $dynhost_ip, $my_ip, @rest_of_log, $logged_etime, $logged_humantime) = (); my ($last_updated_date, $last_updated_date_human); my $datetime = strftime("%Y%m%d-%H%M",localtime()); my $success = 0; my $retries = 1; # For detecting your home IP; this counts up to $max_retries. my $etime = time(); # 29 days is the most often you can update your hostname # without actually changing the IP... and you should do # this every 29 days because after 35 days with no update # they delete your account. my $twentynine_days_ago = $etime - (60 * 60 * 24 * 29); # If we can't connect to the internet, do a short 1-minute timeout here # and then restart the loop, to avoid doing the normal 15-minute timeout # when we can't determine our own IP address (because we're offline). unless(we_are_online_and_DNS_is_working()) { print_func("Going to sleep for 1 minute, then trying again.\n"); sleep 60; next; } clear_file($state_file); # Determine our IP address. We do this by asking 3 randomly-chosen # sites from our list, and making sure they all report the same # IP address for us. my %blacklisted_reporters = (); # for webpages that fail to report our IP, e.g. ones that maybe worked in the past but have stopped working -- blacklist them for the rest of this run, to avoid re-checking them when we know they won't work anyway. while( !($success) ) { if($PREF{ip_detection_command}) { my $ip_from_command = `$PREF{ip_detection_command}`; if(is_ipv4_address($ip_from_command)) { $my_ip = $ip_from_command; print_func("\n$datetime: Detected your IP successfully, using your IP detection command.\nIP: $my_ip\n\n"); $success = 1; } else { $retries++; if($retries > $max_retries) { my $minutes = $sleeptime / 60; print_func("\n$datetime:\nFailed to detect your IP address after $max_retries tries.\n" . "Sleeping for $minutes minutes before trying again.\n\n"); sleep($sleeptime); clear_file($state_file); $retries = 0; next; } print_func("\nCouldn't detect your IP address with certainty; trying again, attempt $retries of $max_retries.\n\n"); $my_ip = "unknown"; $success = 0; } } else { my (@three_hosts,%reported_ips) = (); # This while() ensures that we're checking 3 unique sites to determine our IP: while( $three_hosts[0] == $three_hosts[1] || $three_hosts[1] == $three_hosts[2] || $three_hosts[2] == $three_hosts[0] || $blacklisted_reporters{$three_hosts[0]} || $blacklisted_reporters{$three_hosts[1]} || $blacklisted_reporters{$three_hosts[2]} ) { @three_hosts = ( substr(rand($num_reporters), 0, 1), substr(rand($num_reporters), 0, 1), substr(rand($num_reporters), 0, 1) ); } for(@three_hosts) { my $i = $_; my $reporter_url = $ip_reporters{$i}; my ($host) = ($reporter_url =~ /(.*?)\//); my $sock = IO::Socket::INET->new( PeerAddr => $host, PeerPort => 80, Proto => "tcp", Timeout => $inettimeout ); if($sock) { $sock->autoflush(1); my $request = new HTTP::Request 'GET' => "http://$reporter_url"; my $result = $agent->request($request); my $thepage = $result->content; # Some pages have links that are based on IP addresses instead of TLD names, # which messes up our detection... so remove any html tags, including links: $thepage =~ s/<.*?>//gs; if($thepage =~ /(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/g) { $reported_ips{$i} = $1 unless $1 eq '127.0.0.1'; } else { $blacklisted_reporters{$i} = 1; } } } for(@three_hosts) { print_func("You are $reported_ips{$_}, according to $ip_reporters{$_}.\n"); } if(( $reported_ips{$three_hosts[0]} eq $reported_ips{$three_hosts[1]} && $reported_ips{$three_hosts[1]} eq $reported_ips{$three_hosts[2]} ) && ( ($reported_ips{$three_hosts[0]} =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) && ($reported_ips{$three_hosts[1]} =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) && ($reported_ips{$three_hosts[2]} =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) ) ) { $my_ip = $reported_ips{$three_hosts[0]}; print_func("\n$datetime: Detected your IP successfully, using web-based lookup.\n\n"); $success = 1; } else { $retries++; if($retries > $max_retries) { my $minutes = $sleeptime / 60; print_func("\n$datetime:\nFailed to detect your IP address after $max_retries tries.\n" . "Sleeping for $minutes minutes before trying again.\n\n"); sleep($sleeptime); clear_file($state_file); $retries = 0; next; } print_func("\nCouldn't detect your IP address with certainty; trying again, attempt $retries of $max_retries.\n\n"); $my_ip = "unknown"; $success = 0; } } } send_regular_email_notification($my_ip); if($PREF{hostless_mode} =~ /yes/i) { print_func(qq`Not updating any hosts, since we're in hostless_mode.\n`); next; } for(@hosts_to_update) { my ($service,$thehost) = (/^(.+):(.+)$/); my $optional_stuff; if($PREF{'mx'}) { $optional_stuff .= "&mx=$PREF{'mx'}"; } if($PREF{'backmx'}) { $optional_stuff .= "&backmx=$PREF{'backmx'}"; } if($options{'--offline'}) { $optional_stuff .= "&offline=YES"; } my ($uri, $dyn_service_host) = (); if($service eq 'dyndns') { $dyn_service_host = 'members.dyndns.org'; $uri = "nic/update?system=$PREF{'system'}&hostname=$thehost" . $optional_stuff . '&myip='; } elsif($service eq 'zoneedit') { $dyn_service_host = 'dynamic.zoneedit.com'; #$uri = "auth/dynamic.html?zones=$thehost"; $uri = "auth/dynamic.html?host=$thehost"; } else { die_func(qq`Invalid service name: "$service"\n`); } my $log = $curdir . $scriptname . '--' . $thehost . "--dynamic-ip-addresses.log"; my $last_updated_date_log = $curdir . $scriptname . '--' . $thehost . "--last-updated-date.log"; if(!(-e $log)) { open(OUT,">$log") or die_func("$0: couldn't create $log: $!, you must create it manually.\n"); close OUT; } if(!(-e $last_updated_date_log)) { open(OUT,">$last_updated_date_log") or die_func("$0: couldn't create $last_updated_date_log: $!, you must create it manually.\n"); close OUT; } # Open the last-updated log to see when we last changed our IP on the dyndns host: open(IN,"<$last_updated_date_log") or die_func("$0: couldn't open $last_updated_date_log: $!\n"); flock IN, 2; seek IN, 0, 0; $firstline = ; my @temp = split(/ /, $firstline); $last_updated_date = $temp[0]; $last_updated_date_human = $temp[1]; close IN or die_func("$0: couldn't close $last_updated_date_log: $!\n"); # Check the log to see if this is a new IP: open(IO,"+<$log") or die_func("$0: couldn't open $log: $!\n"); flock IO, 2; seek IO, 0, 0; $firstline = ; chomp $firstline; ($logged_etime, $logged_humantime, $dynhost_ip) = split(/ /, $firstline); @rest_of_log = ; seek IO, 0, 0; my $tries = 0; TryToResolveHostnameAgain: $dynhost_ip = name_to_address($thehost); if( ($dynhost_ip =~ /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/) && ($my_ip =~ /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/) ) { my $logfiles_exist = 0; if($last_updated_date =~ /\d+/) { $logfiles_exist = 1; } my $force_update_to_prevent_expiration = 0; if($logfiles_exist && ($last_updated_date < $twentynine_days_ago) && ($service eq 'dyndns')) { $force_update_to_prevent_expiration = 1; } my ($update_status,$msg) = (); if( ($dynhost_ip ne $my_ip) || ($force_update_to_prevent_expiration) || $options{'--offline'} || $options{'--force'} || !$logfiles_exist) { if($options{'--testmode'}) { print_func("Since --testmode was passed, skipping update for $thehost.\n"); } else { ##### ##### ##### Update $thehost now. ##### $msg = "\n$dynhost_ip is the current IP of $thehost.\n" . "$my_ip is your current IP.\n" . "The last update was $last_updated_date_human.\n"; print_func($msg); $update_status .= $msg; if($force_update_to_prevent_expiration) { $msg = "\nForcing an update now since it's been more than 29 days\nsince the last update, to prevent DynDNS.org from auto-expiring\nyour hostname.\n"; print_func($msg); $update_status .= $msg; } # If an update is necessary, do it now. my $response = (); if(($dynhost_ip ne $my_ip) || $force_update_to_prevent_expiration || $options{'--offline'} || $options{'--force'}) { my $extra = $service eq 'dyndns' ? "$my_ip$wildcard_option" : ''; my $theuri = "https://$dyn_service_host/$uri$extra"; $msg = "About to update $thehost by asking $dyn_service_host for\n$uri$extra\n\n"; print_func($msg); $update_status .= $msg; my ($request,$result) = (); if($service eq 'dyndns') { $request = HTTP::Request->new('GET', $theuri, $header_dyndns); $result = $agent->request($request); } else # zoneedit { $request = HTTP::Request->new('GET', $theuri, $header_zoneedit); $result = $agent->request($request); } $response = $result->content; chomp $response; $msg = "server-response: $response\n"; print_func($msg); $update_status .= $msg; } else # if none of those 4 items were true, then !$logfiles_exist is true. { $msg = "This is the first time you're updating this host, or your logs have been\n" . "deleted. Either way, the IP of $thehost needs no updating.\n\n"; print_func($msg); $update_status .= $msg; $response = 'eponym_firstrun_IP_needs_no_update'; } if($service eq 'dyndns') { if($response =~ /badauth|badsys|badagent|notfqdn|nohost|!donator|!yours|!active|abuse|numhost|dnserr|w(\d+)(h|m|s)|911/i) { if( ($1 =~ /\d+/) && ($2 =~ /h|m|s/i) ) { my $error_msg = '#' x 76 . "\n# ERROR while updating $thehost: $response" . "\n# If you don't know how to fix this, contact us at " . "\n# encodable.com/contact/ and we'll help you with it." . "\n# your ip: $my_ip host ip: $dynhost_ip.\n" . '#' x 76; $error_msg .= "\n\nNOTE: the error was not fatal; attempting to continue.\n"; my $number = $1; my $multiplier = 1; if($2 eq 'h') { $multiplier = 60*60; } if($2 eq 'm') { $multiplier = 60; } if($2 eq 's') { $multiplier = 1; } $sleeptime = $number * $multiplier; print_func($error_msg); # Append the "sleeping for..." string *after* printing the error_msg here, # because we print "sleeping for..." at the end of the loop. We're only # appending it here for the sake of the email. $error_msg .= "\n\nTemporary error; sleeping for $sleeptime seconds because the DynDNS.org error said so.\n"; send_email($thehost, "Error updating $thehost...", $error_msg); } else { my $error_msg = '#' x 76 . "\n# ERROR while updating $thehost: $response" . "\n# If you don't know how to fix this, contact us at " . "\n# encodable.com/contact/ and we'll help you with it." . "\n# your ip: $my_ip host ip: $dynhost_ip.\n" . '#' x 76; do_failed_update_actions($thehost, $response, $error_msg); sleep 20; exit; } } else { if($response =~ /good/i) { do_successful_update_actions($thehost, $my_ip, $dynhost_ip, $etime, $datetime, $update_status, $response, $last_updated_date_log); } elsif($response =~ /nochg/i) { do_redundant_update_actions($thehost, $my_ip, $dynhost_ip, $etime, $datetime, $update_status, $response, $last_updated_date_log); } elsif($response =~ /eponym_firstrun_IP_needs_no_update/i) { do_firstrun_actions($thehost, $my_ip, $dynhost_ip, $etime, $datetime, $update_status, $response, $last_updated_date_log); } else { do_failed_update_actions($thehost, "Unknown error; server response: $response; your ip: $my_ip; host ip: $dynhost_ip"); sleep 20; exit; } $sleeptime = $sleeptime_default; # Normal update interval. } } elsif($service eq 'zoneedit') { if($response =~ /SUCCESS CODE="200"/) { do_successful_update_actions($thehost, $my_ip, $dynhost_ip, $etime, $datetime, $update_status, $response, $last_updated_date_log); } elsif($response =~ /(?:ERROR|SUCCESS) CODE="(?:201|707)"/) { do_redundant_update_actions($thehost, $my_ip, $dynhost_ip, $etime, $datetime, $update_status, $response, $last_updated_date_log); } elsif($response =~ /eponym_firstrun_IP_needs_no_update/) { do_firstrun_actions($thehost, $my_ip, $dynhost_ip, $etime, $datetime, $update_status, $response, $last_updated_date_log); } elsif($response =~ /ERROR CODE="70\d"/) { do_failed_update_actions($thehost, "$response; your ip: $my_ip; host ip: $dynhost_ip"); sleep 20; exit; } elsif($response =~ /^5\d\d/) { do_failed_update_warning($thehost, "Unknown error; server response: $response; Your ip: $my_ip; host ip: $dynhost_ip"); sleep 20; } elsif($response !~ /\w/) { do_failed_update_actions($thehost, "Server did not respond; this probably means your authorization failed. Your ip: $my_ip; host ip: $dynhost_ip"); sleep 20; exit; } else { do_failed_update_actions($thehost, "Unknown error; server response: $response; your ip: $my_ip; host ip: $dynhost_ip"); sleep 20; exit; } $sleeptime = $sleeptime_default; # Normal update interval. } else { die qq`Invalid service name: "$service"\n`; } eval $extra_code; # Also write this new IP address to the log file: print IO "$etime $datetime $my_ip\n"; # Write the old firstline as the new second line: print IO "$firstline\n"; } } else { print_func("$datetime:\n$thehost needs no update. It's $dynhost_ip, you're $my_ip\n\n"); # Just update the datetime (currentip is the same as lastip): print IO "$etime $datetime $my_ip\n"; } } else { if($tries < 3) { $tries++; goto TryToResolveHostnameAgain; } my $error_msg = "There was a problem detecting one of these IP addresses:\n$thehost: $dynhost_ip\nyour IP: $my_ip\n\nUpdate skipped. Eponym will continue to run because this might be a\ntemporary network outage; however if you get this message lots of times\nthere is probably a problem.\n\n"; print_func($error_msg); send_email($thehost, "Problem resolving dynamic-dns hostname...", $error_msg); # Also write the error to the log file: print IO "$etime $datetime could_not_detect_ip_for_$thehost your_ip_is_$my_ip\n"; # Write the old firstline as the new second line: print IO "$firstline\n"; } print IO @rest_of_log; truncate IO, tell IO; close IO or die_func("$0: couldn't close $log: $!\n"); } # end for(@hosts_to_update) } continue { if($options{'--force'}) { print_func("\nExiting since --force was used. Won't force-update hostnames repeatedly.\n"); exit; } elsif($options{'--forever'}) { print_func("Going to sleep for " . ($sleeptime/60) . " minutes; next update at " . strftime("%I:%M",localtime( time()+$sleeptime )) . ".\n\n" . "*" x 76 . "\n"); sleep($sleeptime); } else { print_func("\nExiting.\n"); exit; } } # end while(1) sub name_to_address($) { my $name = shift; # we allow hostnames like *.foo.com to be updated, so to do the lookup on that, # just make up a random subdomain $name =~ s!^\*\.!'testsub-' . strftime("%Y%m%d-%H%M%S",localtime) . '.'!e; # local (@octets); # # local ($nam, $aliases, $addrtype, $length, $address) = # gethostbyname ($name); # # if (! length ($address)) { # $addy = "unknown"; # return $addy;} # # @octets = unpack ("CCCC", $address); # # $addy = join ('.', @octets[0..3]), "\n"; # # return $addy; # 20040917, scottcarlson //at// yahoo //dot// com: # Use Net::DNS::Resolver instead of gethostbyname() # because then people can do things in /etc/hosts # and they won't interfere with our name resolutions. my $res = Net::DNS::Resolver->new; $res->tcp_timeout(10); my $query = $res->search($name); my $address = ''; if($query) { foreach my $rr ($query->answer) { next unless $rr->type eq "A"; $address = $rr->address; } } $address ||= "unknown"; #print "name_to_address($name): returning '$address'\n"; return $address; } sub load_prefs() { my ($pref, $value); open(IN,"<$prefs_file") or die_func("$0: couldn't open $prefs_file: $!\n"); flock IN, 2; seek IN, 0, 0; while() { chomp; # Skip lines that are blank, and comments: if(/(^#|^\s+$)/) { next; } ($pref, $value) = split(/=/, $_, 2); next if $value !~ /\S/; # Strip leading and trailing spaces. for($pref, $value) { s/\s+$//g; s/^\s+//g; } # Special logic for these because they can have multiple values: if($pref eq 'host_to_update') { push @hosts_to_update, $value; } elsif($pref eq 'ip_reporter') { $value =~ s!^(https?|ftp)://!!i; # remove http:// or https:// or ftp:// from the front, if it's there $ip_reporters{(scalar keys %ip_reporters)} = $value; } elsif($pref eq 'extra_code') { $extra_code .= $value; } else { $PREF{$pref} = $value; } } close IN or die_func("$0: couldn't close $prefs_file: $!\n"); $PREF{num_minutes_between_regular_email_notifications} = 60 * 3 unless $PREF{num_minutes_between_regular_email_notifications} =~ /^\d+$/; # 3 hours by default. } sub verify_prefs() { if($PREF{'system'} !~ /^(dyndns|statdns|custom)$/) { print_func("Your \"system\" preference is set to something invalid. It must be\n" . "either dyndns, statdns, or custom, but it's currently set to:\n\n" . $PREF{'system'} . "\n\nQuitting now.\n"); sleep 20; exit; } if($PREF{'backmx'} !~ /^(YES|NO|)$/) { print_func("Your \"backmx\" preference is set to something invalid. It must be\n" . "either YES, NO, or blank, but it's currently set to:\n\n" . $PREF{'backmx'} . "\n\nQuitting now.\n"); sleep 20; exit; } } sub send_email($$$) { return unless $PREF{smtp_server} =~ /\w/; my $thishost = shift; my $subject = shift; my $message = shift; foreach my $recipient (sort keys %PREF) { next unless $recipient =~ /^email_recipient_\d+$/; next unless $PREF{$recipient} =~ /.+\@.+\..+/; $thishost =~ s/\*\.//g; # allow for "*.foo.com" as a wildcard ZoneEdit hostname. my $sender = $thishost ? 'eponym@' . $thishost : $PREF{$recipient}; my $mime_msg = MIME::Lite->new( From => "Eponym Script <$sender>", To => $PREF{$recipient}, Subject => $subject, Type => 'TEXT', Data => "$message\n\n[Sent by Eponym v$version]\n" ) or print_func("$0: error creating MIME body: $!\nAttempting to continue since email-related problems do not affect Eponym's core functionality; however you should look into this problem.\n"); $PREF{email_hello_domain} ||= $ENV{HTTP_HOST}; # set it to this website's domain name, instead of the default "localhost.localdomain", which might help avoid getting flagged as spam. if($PREF{smtp_auth_username} =~ /\S/ && $PREF{smtp_auth_password} =~ /\S/) { eval { MIME::Lite->send('smtp', $PREF{smtp_server}, Timeout=>30, Hello=>$PREF{email_hello_domain}, AuthUser=>$PREF{smtp_auth_username}, AuthPass=>$PREF{smtp_auth_password}); }; } else { eval { MIME::Lite->send('smtp', $PREF{smtp_server}, Timeout=>30, Hello=>$PREF{email_hello_domain}); }; } print_func("$0: MIME::Lite->send failed: $@\nAttempting to continue since email-related problems do not affect Eponym's core functionality; however you should look into this problem.\n") if $@; if(we_are_online_and_DNS_is_working()) { eval { $mime_msg->send; }; if($@) { print_func("$0: \$mime_msg->send failed: $@\nAttempting to continue since email-related problems do not affect Eponym's core functionality; however you should look into this problem.\n"); } else { print_func("Sent email to $PREF{$recipient} via SMTP server $PREF{smtp_server}.\n\n"); } } } } sub send_regular_email_notification($) { return unless $PREF{smtp_server} =~ /\w/; my $time_elapsed_since_last_regular_email = time - get_eponym_data('time_of_last_regular_email'); my $num_seconds_between_regular_email_notifications = $PREF{num_minutes_between_regular_email_notifications} * 60; #print_func(qq`\$time_elapsed_since_last_regular_email=$time_elapsed_since_last_regular_email \n\$num_seconds_between_regular_email_notifications=$num_seconds_between_regular_email_notifications \nin send_regular_email_notification\n`); return if $time_elapsed_since_last_regular_email < $num_seconds_between_regular_email_notifications; my $my_ip = shift; my ($thishost, $allhosts) = (); my $i = 0; for(@hosts_to_update) { my ($service,$host) = split /:/, $_; $thishost = $host if $i == 0; $allhosts .= $host . ', '; $i++; } $allhosts =~ s/, $//; my $multiple_hosts = $allhosts eq $thishost ? () : ' (...)'; my $subject = "IP for ${thishost}${multiple_hosts}: $my_ip"; my $message = "This is a regular notification of the IP address of the following host(s):\n\n$allhosts\n\nThe IP is currently: $my_ip\n"; foreach my $recipient (sort keys %PREF) { next unless $recipient =~ /^email_recipient_(\d+)$/; my $rnum = $1; next unless $PREF{"recipient_${rnum}_gets_regular_notifications"} =~ /yes/i; next unless $PREF{$recipient} =~ /.+\@.+\..+/; my $sender = $thishost ? 'eponym@' . $thishost : $PREF{$recipient}; my $mime_msg = MIME::Lite->new( From => "Eponym Script <$sender>", To => $PREF{$recipient}, Subject => $subject, Type => 'TEXT', Data => "$message\n\n[Sent by Eponym v$version]\n" ) or print_func("$0: error creating MIME body: $!\nAttempting to continue since email-related problems do not affect Eponym's core functionality; however you should look into this problem.\n"); $PREF{email_hello_domain} ||= $ENV{HTTP_HOST}; # set it to this website's domain name, instead of the default "localhost.localdomain", which might help avoid getting flagged as spam. if($PREF{smtp_auth_username} =~ /\S/ && $PREF{smtp_auth_password} =~ /\S/) { eval { MIME::Lite->send('smtp', $PREF{smtp_server}, Timeout=>30, Hello=>$PREF{email_hello_domain}, AuthUser=>$PREF{smtp_auth_username}, AuthPass=>$PREF{smtp_auth_password}); }; } else { eval { MIME::Lite->send('smtp', $PREF{smtp_server}, Timeout=>30, Hello=>$PREF{email_hello_domain}); }; } print_func("$0: MIME::Lite->send failed: $@\nAttempting to continue since email-related problems do not affect Eponym's core functionality; however you should look into this problem.\n") if $@; if(we_are_online_and_DNS_is_working()) { eval { $mime_msg->send; }; if($@) { print_func("$0: \$mime_msg->send failed: $@\nAttempting to continue since email-related problems do not affect Eponym's core functionality; however you should look into this problem.\n"); } else { print_func("Sent email to $PREF{$recipient} via SMTP server $PREF{smtp_server}.\n\n"); log_eponym_data('time_of_last_regular_email', time); } } } } sub we_are_online_and_DNS_is_working { my $online = 0; for('www.google.com', 'www.microsoft.com', 'www.yahoo.com', 'www.cnn.com', 'www.aol.com', 'www.amazon.com', 'www.ebay.com') { my $sock = IO::Socket::INET->new( PeerAddr => $_, PeerPort => 80, Proto => "tcp", Timeout => "10" ); if($sock) { $sock->autoflush(1); close( $sock ); $online = 1; last; } } if(!$online) { my $datetime = strftime("%Y%m%d-%H:%M:%S",localtime()); print_func("\n$datetime\nWARNING: it looks like we're offline...\n\n"); } return $online; } sub do_successful_update_actions { my ($thehost, $my_ip, $dynhost_ip, $etime, $datetime, $update_status, $response, $last_updated_date_log) = @_; print_func("Updated the IP of $thehost to $my_ip successfully.\n\n"); send_email($thehost, "Updated IP for $thehost to $my_ip", "$update_status\n\nNote: this message is just a notice that a successful update took place. You do not need to take any action.\n"); open(IO2,"+<$last_updated_date_log") or die_func("$0: couldn't open $last_updated_date_log: $!\n"); flock IO2, 2; seek IO2, 0, 0; my @temp = ; seek IO2, 0, 0; print IO2 "$etime $datetime currentip=$my_ip lastip=$dynhost_ip response: $response\n"; print IO2 @temp; truncate IO2, tell IO2; close IO2 or die_func("$0: couldn't close $last_updated_date_log: $!\n"); } sub do_redundant_update_actions { my ($thehost, $my_ip, $dynhost_ip, $etime, $datetime, $update_status, $response, $last_updated_date_log) = @_; my $msg = "Redundant update: $thehost to $my_ip"; print_func("$msg\n\n"); send_email($thehost, $msg, "$update_status\n\nNote: this message is not an indication of a problem unless it happens frequently.\n"); open(IO2,"+<$last_updated_date_log") or die_func("$0: couldn't open $last_updated_date_log: $!\n"); flock IO2, 2; seek IO2, 0, 0; my @temp = ; seek IO2, 0, 0; print IO2 "$etime $datetime currentip=$my_ip lastip=$dynhost_ip response: $response\n"; print IO2 @temp; truncate IO2, tell IO2; close IO2 or die_func("$0: couldn't close $last_updated_date_log: $!\n"); } sub do_firstrun_actions { my ($thehost, $my_ip, $dynhost_ip, $etime, $datetime, $update_status, $response, $last_updated_date_log) = @_; my $msg = "Eponym first run: $thehost = $my_ip"; print_func("$msg\n\n"); send_email($thehost, $msg, "$update_status\n\nNote: this message is not an indication of a problem unless it happens repeatedly for the same host(s).\n"); open(IO2,"+<$last_updated_date_log") or die_func("$0: couldn't open $last_updated_date_log: $!\n"); flock IO2, 2; seek IO2, 0, 0; my @temp = ; seek IO2, 0, 0; print IO2 "$etime $datetime currentip=$my_ip lastip=$dynhost_ip response: $response\n"; print IO2 @temp; truncate IO2, tell IO2; close IO2 or die_func("$0: couldn't close $last_updated_date_log: $!\n"); } sub do_failed_update_actions { my ($thehost, $response, $error_msg) = @_; if(!(-e $error_log)) { open(OUT,">$error_log") or die_func("$0: couldn't create $error_log: $!, you must create it manually.\n"); close OUT; } open(OUT,">>$error_log") or die_func("$0: couldn't open $error_log for appending: $!\n"); flock OUT, 2; seek OUT, 0, 2; my $etime = time(); my $datetime = strftime("%Y%m%d-%H%M",localtime($etime)); my $more_error = "$etime $datetime Error while updating $thehost: "; print OUT $more_error . $response . "\n"; close OUT or die_func("$0: couldn't close $error_log: $!\n"); $error_msg .= "\n$more_error\n$response\n"; $error_msg .= "\nThis error could be a temporary server problem, so you might want" . "\nto try again in a little while. If the problem persists, visit" . "\nencodable.com/eponym/ for the latest version, or" . "\ncontact us at encodable.com/contact/ for help." . qq`\n` . qq`\nNOTE: Eponym is quitting now, so you'll need to take action` . qq`\n(either fix this problem, or just start Eponym again) or else` . qq`\nyour hostnames will not be updated anymore.` . qq`\n`; print_func($error_msg); send_email($thehost, "Error updating $thehost...", $error_msg); } sub do_failed_update_warning { my ($thehost, $response, $error_msg) = @_; if(!(-e $error_log)) { open(OUT,">$error_log") or die_func("$0: couldn't create $error_log: $!, you must create it manually.\n"); close OUT; } open(OUT,">>$error_log") or die_func("$0: couldn't open $error_log for appending: $!\n"); flock OUT, 2; seek OUT, 0, 2; my $etime = time(); my $datetime = strftime("%Y%m%d-%H%M",localtime($etime)); my $more_error = "$etime $datetime Error while updating $thehost: "; print OUT $more_error . $response . "\n"; close OUT or die_func("$0: couldn't close $error_log: $!\n"); $error_msg .= "\n$more_error\n$response\n"; $error_msg .= "\nThis error could be a temporary server problem, so Eponym is not" . "\ngoing to exit; we'll keep trying. If the problem persists, visit" . "\nencodable.com/eponym/ for the latest version, or" . "\ncontact us at encodable.com/contact/ for help." . qq`\n`; print_func($error_msg); send_email($thehost, "Error updating $thehost...", $error_msg); } sub write_pid_file($) { my $file = shift; my $datetime = strftime("%Y%m%d-%H:%M:%S",localtime(time)); open(OUT,">$file") or die_func("$0: couldn't open $file for writing: $!\n"); flock OUT, 2; seek OUT, 0, 0; print OUT "$datetime pid=$$\n"; close OUT or die_func("$0: couldn't close $file: $!\n"); } sub print_func($) { my $output = shift; print $output; open(APPEND,">>$state_file") or die "$0: couldn't open $state_file for appending: $!\n"; flock APPEND, 2; print APPEND $output; close APPEND or die "$0: couldn't close $state_file: $!\n"; } sub clear_file($) { my $file = shift; open(OUT,">$file") or die "$0: couldn't open $file for writing: $!\n"; flock OUT, 2; seek OUT, 0, 0; truncate OUT, tell OUT; close OUT or die "$0: couldn't close $file: $!\n"; } sub die_func($) { my $output = shift; my $death_msg = "\n\nEponym exited with an error.\n"; print_func($output . $death_msg); die ""; } sub log_eponym_data { my ($item, $value) = @_; if(! -e $data_file) { open(my $outfh,">$data_file") or die "$0: couldn't create $data_file: $!\n"; close $outfh or die "$0: couldn't close $data_file after creating it: $!\n"; } my $logged = (); open(my $iofh, "+<$data_file") or die "$0: couldn't open $data_file for R/W: $!\n"; flock $iofh, 2; seek $iofh, 0, 0; my @old_contents = <$iofh>; seek $iofh, 0, 0; for(@old_contents) { if(/^$item:/) { print $iofh "$item: $value\n"; $logged = 1; } else { print $iofh $_; } } print $iofh "$item: $value\n" unless $logged; truncate $iofh, tell $iofh; close $iofh or die "$0: couldn't close $data_file after R/W: $!\n"; } sub get_eponym_data { my ($item) = @_; if(! -e $data_file) { return; } my $value = (); open(my $infh, "<$data_file") or die "$0: couldn't open $data_file for reading: $!\n"; flock $infh, 1; seek $infh, 0, 0; while(<$infh>) { chomp; if(/^$item:\s*(.*)/) { $value = $1; last; } } close $infh or die "$0: couldn't close $data_file after reading: $!\n"; return $value; } sub is_ipv4_address { return $_[0] =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; }