#!/bin/bash
#
#   RegMyIP -- DynDNS client 
#
#   Use: Configure and let crontab run it each 10 minutes.
#
#   © Copyright 2014 Arto Jääskeläinen <arto.jaaskelainen(at)pp.inet.fi>
#   All rights reserved.
#   The program is distributed under the terms of the GNU General Public License
#
#   RegMyIP 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 3 of the License, or
#   (at your option) any later version.
#
#   RegMyIP 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.
#
#   For further information see <http://www.gnu.org/licenses/>.
#
########################################################################
#
#   v1.0  23.09.2014/Arto  initial "curl" based version
#   v1.1  07.10.2014/Arto  cross check, "dnsexit" added
#   v1.2  08.10.2014/Arto  multiple SPs on same host
#   v1.3  09.10.2014/Arto  more response codes
#   v1.4  11.10.2014/Arto  multi domain reg for IP, single client
#   v1.5  12.10.2014/Arto  Single Message Multi Domain feature
#   v1.6  15.10.2014/Arto  typo fix
#   v1.7  16.10.2014/Arto  detection of route
#   v1.8  17.10.2014/Arto  more log messages
#   v1.9  21.10.2014/Arto  encrypted uid file support
#   v2.0  23.10.2014/Arto  build-in configuration utility
#   v2.1  28.10.2014/Arto  bug fix update
#   v2.2  14.03.2015/Arto  additional check for hidden dir
#   v2.3  26.09.2016/Arto  curl -k to avoid error 60 with https
#   v2.4  19.10.2017/Arto  fix for multiple default gw situation 
#   v2.5  20.10.2017/Arto  cosmetic changes to configuration utility
#   v2.6  22.02.2018/Arto  gw may point to local ip but still public ip exists
#   v2.7  16.09.2020/Arto  Openssl bug "WARNING : deprecated key derivation used"
#
########################################################################
client_name="RegMyIP generic DDNS client" 
client_version="v2.7"
client_release="GPL3 release"

# Let's define location for each util
# ...just in case
grep()
{
/bin/grep "$@"
}
sed()
{
/bin/sed "$@"
}
netstat()
{
/bin/netstat "$@"
}
cut()
{
/usr/bin/cut "$@"
}
tr()
{
/usr/bin/tr "$@"
}
route()
{
/sbin/route "$@"
}

explain_curl_result()
{
case "$1" in
1) e_txt="Unsupported protocol. This build of curl has no support for this protocol." ;;
2) e_txt="Failed to initialize." ;;
3) e_txt="URL malformat. The syntax was not correct." ;;
4) e_txt="URL user malformatted. The user-part of the URL syntax was not correct." ;;
5) e_txt="Couldn't resolve proxy. The given proxy host could not be resolved." ;;
6) e_txt="Couldn't resolve host. The given remote host was not resolved." ;;
7) e_txt="Failed to connect to host." ;;
8) e_txt="FTP weird server reply. The server sent data curl couldn't parse." ;;
9) e_txt="FTP access denied. The server denied login." ;;
10) e_txt="FTP user/password incorrect. Either one or both were not accepted by the server." ;;
11) e_txt="FTP weird PASS reply. Curl couldn't parse the reply sent to the PASS request." ;;
12) e_txt="FTP weird USER reply. Curl couldn't parse the reply sent to the USER request." ;;
13) e_txt="FTP weird PASV reply, Curl couldn't parse the reply sent to the PASV request." ;;
14) e_txt="FTP weird 227 format. Curl couldn't parse the 227-line the server sent." ;;
15) e_txt="FTP can't get host. Couldn't resolve the host IP we got in the 227-line." ;;
16) e_txt="FTP can't reconnect. Couldn't connect to the host we got in the 227-line." ;;
17) e_txt="FTP couldn't set binary. Couldn't change transfer method to binary." ;;
18) e_txt="Partial file. Only a part of the file was transfered." ;;
19) e_txt="FTP couldn't download/access the given file, the RETR (or similar) command failed." ;;
20) e_txt="FTP write error. The transfer was reported bad by the server." ;;
21) e_txt="FTP quote error. A quote command returned error from the server." ;;
22) e_txt="HTTP page not retrieved. The requested url was not found or returned another error with the HTTP error code being 400 or above. This return code only appears if -f/--fail is used." ;;
23) e_txt="Write error. Curl couldn't write data to a local filesystem or similar." ;;
24) e_txt="Malformed user. User name badly specified." ;;
25) e_txt="FTP couldn't STOR file. The server denied the STOR operation, used for FTP uploading." ;;
26) e_txt="Read error. Various reading problems." ;;
27) e_txt="Out of memory. A memory allocation request failed." ;;
28) e_txt="Operation timeout. The specified time-out period was reached according to the conditions." ;;
29) e_txt="FTP couldn't set ASCII. The server returned an unknown reply." ;;
30) e_txt="FTP PORT failed. The PORT command failed. Not all FTP servers support the PORT command, try doing a transfer using PASV instead!" ;;
31) e_txt="FTP couldn't use REST. The REST command failed. This command is used for resumed FTP transfers." ;;
32) e_txt="FTP couldn't use SIZE. The SIZE command failed. The command is an extension to the original FTP spec RFC 959." ;;
33) e_txt="HTTP range error. The range _command_ didn't work." ;;
34) e_txt="HTTP post error. Internal post-request generation error." ;;
35) e_txt="SSL connect error. The SSL handshaking failed." ;;
36) e_txt="FTP bad download resume. Couldn't continue an earlier aborted download." ;;
37) e_txt="FILE couldn't read file. Failed to open the file. Permissions?" ;;
38) e_txt="LDAP cannot bind. LDAP bind operation failed." ;;
39) e_txt="LDAP search failed." ;;
40) e_txt="Library not found. The LDAP library was not found." ;;
41) e_txt="Function not found. A required LDAP function was not found." ;;
42) e_txt="Aborted by callback. An application told curl to abort the operation." ;;
43) e_txt="Internal error. A function was called with a bad parameter." ;;
44) e_txt="Internal error. A function was called in a bad order." ;;
45) e_txt="Interface error. A specified outgoing interface could not be used." ;;
46) e_txt="Bad password entered. An error was signaled when the password was entered." ;;
47) e_txt="Too many redirects. When following redirects, curl hit the maximum amount." ;;
48) e_txt="Unknown TELNET option specified." ;;
49) e_txt="Malformed telnet option." ;;
51) e_txt="The remote peer's SSL certificate wasn't ok" ;;
52) e_txt="The server didn't reply anything, which here is considered an error." ;;
53) e_txt="SSL crypto engine not found" ;;
54) e_txt="Cannot set SSL crypto engine as default" ;;
55) e_txt="Failed sending network data" ;;
56) e_txt="Failure in receiving network data" ;;
57) e_txt="Share is in use (internal error)" ;;
58) e_txt="Problem with the local certificate" ;;
59) e_txt="Couldn't use specified SSL cipher" ;;
60) e_txt="Problem with the CA cert (path? permission?)" ;;
61) e_txt="Unrecognized transfer encoding" ;;
62) e_txt="Invalid LDAP URL" ;;
63) e_txt="Maximum file size exceeded" ;;
esac
}

valid_ip()
# Exit code true if match
{
local  ip="$1"
stat=1
if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} ]]; then
	oldifs="$IFS"
	IFS='.' ip_array=(${ip[@]})
	IFS="$oldifs"
	[[ 10#${ip_array[0]} -le 255 && 10#${ip_array[1]} -le 255 && 10#${ip_array[2]} -le 255 && 10#${ip_array[3]} -le 255 ]]
	# 10#n notation is mandatory to avoid bash interpreting leading zero numbers as octal
	stat="$?"
fi
return "$stat"
}

private_ip()
# Exit code true if match
# Line must start with valid ip address, any text after that is ignored
{
grep -q -E '\\
(^127\.)|\\
(^10\.)|\\
(^172\.1[6-9]\.)|\\
(^172\.2[0-9]\.)|\\
(^172\.3[0-1]\.)|\\
(^192\.168\.)' <<< "$1"
return "$?"
}

########################################
any_public_ip()
#Pick one public ip
#Entry: LF separated list of ip addresses + interface
#Return: gw ip and eth interface name nic_id
#Return "" if no public ip found 
{
local line
unset public_ip
while IFS= read -r line ; do
	if ! private_ip "$line"; then
		public_ip="$line"
	fi
done <<< "$1"
}

get_our_default_gw()
# Return examples:
# 172.31.1.1 eth0		(=public ip)
# 192.168.1.1 enp3s0   	(=private ip)
{
gw_list=$(route -n | grep "^0.0.0.0" |tr -s ' '|cut -d ' ' -f2,8)
}

########################################

get_quoted_text()
# variablename="some_quoted_string"
#$1=variable name
#$2=file name
#
{
result="$(cut -d '"' -f2 <<<  $(grep $1= $2 2>/dev/null))"	
}

get_value()
# variablename=12345
#$1=variable name
#$2=file name
#
{
result="$(cut -d '=' -f2 <<<  $(grep $1= $2 2>/dev/null))"	
}

edit()
#Typing bare Enter means no change
#$1 = prompt
#$2 = textline to edit
{
echo "$1: $2"
read -p "Give new one, Enter=no change: " text
result="${text:-$2}"
}

append_string()
#par1=name, par2=string, par3=name
{
if [ -z "${!1}" ]; then
eval "${1}='$2'"
else
eval "${1}='${!1}''${!3}''$2'"
fi
}

build_sp_fields()
# We build here request fields depending on service provider requirements
# This allows adopting to various SP structures and adding new ones
{
unset request
case "$service_provider" in
dyfi)
		update_link=""		# dy.fi uses fixed url for updates
		request[0]="--user"
		request[1]="$my_login:$my_pw"
		request[2]="https://www.dy.fi/nic/update?hostname=$domains_to_update"
		;;
dnsexit)
		update_link="$(curl --interface $nic_id -k -s -D - http://www.dnsexit.com/ipupdate/dyndata.txt |tr -d '\r' | grep "url=" | sed 's/url=//')"
		request[0]="$update_link?login=$my_login&password=$my_pw&host=$domains_to_update"
		;;
*)	
		echo "$datetime  Unsupported service provider name $service_provider" | tee -a "$logpath" 
		echo "$datetime  RegMyIP DDNS client $client_version error exit" | tee -a "$logpath" 
		exit 67
		;;
esac
}

stamp_done()
# Stores datestamp for each successfully updated domain
# Needed only for expiry avoidance functionality
{
IFS=';' read -ra dom <<< "$domains_to_update"
for i in "${dom[@]}"; do
    touch "$date_path/$i"
done
}

update_ddns()
{
local success=false
local attempts=$attempts_max
until [ $attempts = 0 ]; do
	datetime=$(date "+%F_%T")
	http_response=$(curl --interface $nic_id -k -s -A"$my_ua" -D - "${request[@]}")
	result=$?
	echo "$http_response" > "$dir_http_responses/$service_provider.uresp"
	if [ $result = 0 ]; then	# curl received some response
	attempts=0; success=true
	oldifs="$IFS"
	IFS= ret_code=$(echo $http_response | tr -d '\r'| grep -E "$codelist")
	IFS="$oldifs"
	case "$ret_code" in
		nochg*) stamp_done
				echo $ip_web > "$save_ip_path"
				echo "$datetime  Server replied: nochg, ip address is the same as before" | tee -a  "$logpath"  
				;;
		good*)
				stamp_done
				echo $ip_web > "$save_ip_path"
				echo "$datetime  Server replied: $ret_code" | tee -a "$logpath" 
				echo "$datetime  Address update $domains_to_update = $ip_web done" | tee -a "$logpath" 
				;;
		abuse*) echo "$datetime  Server replied: abuse, host has been blocked" | tee -a  "$logpath"  ;;
		badagent*) echo "$datetime  Server replied: badagent, useragent is blocked" | tee -a  "$logpath"   ;;
		badauth*) echo "$datetime  Server replied: badauth, user/pass pair bad" | tee -a  "$logpath"   ;; 
		badip*) echo "$datetime  Server replied: badip, invalid ip address" | tee -a  "$logpath"   ;;
		badsys*) echo "$datetime  Server replied: badsys, bad system parameter" | tee -a  "$logpath"   ;;
		dnserr*) echo "$datetime  Server replied: dnserr, DNS inconsistency" | tee -a  "$logpath"   ;;
		!donator*) echo "$datetime  Server replied: !donator, paid account feature" | tee -a  "$logpath"   ;;
		nohost*) echo "$datetime  Server replied: nohost, no such host in system" | tee -a  "$logpath"   ;;
		notfqdn*) echo "$datetime  Server replied: notfqdn, invalid hostname format" | tee -a  "$logpath"   ;;
		numhost*) echo "$datetime  Server replied: numhost, serious error" | tee -a  "$logpath"   ;;
		!yours*) echo "$datetime  Server replied: !yours, host not in this account" | tee -a  "$logpath"   ;;
		911*) echo "$datetime  Server replied: 911, problem or scheduled maintenance" | tee -a  "$logpath"   ;;
		*) 		
				number_response=$(echo "$http_response" |grep -E ^[0-9].*)
				if [ -z "$number_response" ]; then
				echo "$datetime  Unknown server response, see file $service_provider.uresp" | tee -a "$logpath" 
				else
				echo "$datetime  Server replied: $number_response" | tee -a "$logpath" 
				stamp_done
				echo $ip_web > "$save_ip_path"
				fi
				;;
	esac
	else
		explain_curl_result $result
		echo "$datetime  Address update $domains_to_update failed, curl error $result $e_txt" | tee -a  "$logpath" 
		echo -e "\nCurl was not able to connect, see error log $logpath\n"
		attempts=$[attempts-1]
		[[ $attempts -gt 0 ]] && for ((i=$a_moment; i>0; i--)); do echo -en "Wait... $i \r"; sleep 1; echo -en "         \r"; done
	fi
done
if [ ! $success ]; then
	echo "$datetime   TIMEOUT: Could not connect to service_provider after trying $attempts times using $a_moment seconds pause." | tee -a "$logpath" 
	action_failed_registry
	echo "$datetime  RegMyIP DDNS client $client_version error exit" | tee -a "$logpath" 
	exit 64
fi
}

action_first_time()
# We pass all domains when client is run first time ever
{
echo "$datetime  First time run, $my_domains = $ip_web" | tee -a "$logpath" 
domains_to_update="$my_domains"
action_changed
echo "$datetime  RegMyIP DDNS client $client_version first time exit" | tee -a "$logpath" 
exit "$?"
}

action_no_change()
# Do whatever is needed when ip is still the same as before
{
echo "$datetime  Check done, no change, $my_domains = $ip_web" | tee -a  "$extra_logpath"
}

action_changed()
# On entry:
#  We have list of domains in $domains_to_update 
# We send single update message for multi domain if SMMD is supported.
# Otherwise we save domains in "$domains_to_update" into an array
# and modify "$domains_to_update" to have only one domain at a time
{

if [ -e "$save_ip_path" ]; then	 # not first time run
	echo "$datetime  Web ip $ip_web, dns ip $ip_dns, file ip $(cat "$save_ip_path")" | tee -a "$logpath" 
fi

if [ "$smmd" = "true" ]; then
	echo "$datetime  Using SMMD $domains_to_update = $ip_web, service provider is $service_provider" | tee -a "$logpath" 
	build_sp_fields
	update_ddns
else			# Send one by one
	echo "$datetime  SMMD not enabled, service provider is $service_provider" | tee -a "$logpath" 
	oldifs="$IFS"
	IFS=';' array_update_domains=(${domains_to_update[@]})
	IFS="$oldifs"
	for i in "${!array_update_domains[@]}"; do
	unset domains_to_update
	domains_to_update="${array_update_domains[$i]}"
	echo "$datetime  Sending $domains_to_update = $ip_web" | tee -a "$logpath" 
	build_sp_fields
	update_ddns
	done
fi
}

extra_action_communication_failure()
# Hook for communication failure activity.
# Do not modify unless you are sure how to do it the right way
# i.e.  ( your_thing_to_do ) & 
{
:
}

action_failed_registry()
# Hook for registration failure activity.
# Do not modify unless you are sure how to do it the right way
# i.e.  ( your_thing_to_do ) & 
{
:
}

mismatch_warning()
{
datetime=$(date "+%F_%T")
echo "$datetime    WARNING: DNS returned different ip than the one this client registered $(stat -c%y $date_path/${array_my_domains[i]} | sed 's/\..*//')" | tee -a "$logpath" 
echo "$datetime  This client registered $saved_ip but $ip_dns ip address  was found registered." | tee -a "$logpath" 
echo "$datetime  Are you possibly running DDNS client in another location for this host ?" | tee -a "$logpath" 
echo "$datetime  Your DDNS account may be compromised unless registration change was done by you !" | tee -a "$logpath" 
}


expiry_avoidance()
# Some DDNS providers require you to do send update latest once a week or your registration will expire.
# On the other hand, if you attempt to refresh registration too often you will be banned as abuser.
# We use here 5 days as a default, i.e. ip address update is sent always after 5 days if the ip
# address update has not been sent before that (due to ip address change).
# Storage structure: .updated/<service_provider>/<domain>
# This non urgent update operation is done one domain at a time when refresh for that domain is due.
{
# Is refresh wanted ?
[[ "$refresh_time" = "0" ]] && return

oldifs="$IFS"; IFS=';'
for domain in "${array_my_domains[@]}"; do
	# Did someone delete the date file ? Refresh instantly.
	if [ ! -e "$date_path/$domain" ]; then
	echo "$datetime  Doing date file missing update $domain" | tee -a "$logpath" 
	domains_to_update="$domain"
	action_changed
	fi
	if [ ! -e "$date_path/$domain" ]; then
	echo "$datetime  Error: No date file $date_path/$domain --- please use: regmyip $domain --configure for configuration" | tee -a "$logpath" 
	echo "$datetime  RegMyIP DDNS client $client_version error exit" | tee -a "$logpath" 
	exit 66			# Date file missing, error situation, maybe manually modified .conf file ?)
	fi
done
IFS="$oldifs"
for domain in "$(basename "$(ls -A "$date_path"/* 2>/dev/null)")"; do	
	age_of_date_file=$[$(date +%s) - $(date -r "$date_path/$domain" +%s)]
	if [ $age_of_date_file -gt $refresh_time ]; then
	echo "$datetime  Doing expiry avoidance update $domain" | tee -a "$logpath" 
	domains_to_update="$domain"
	action_changed
	fi
done
}

load_config_file()
{
	if [ -f "$config_file" ]; then
	source "$config_file"
	else
	case "$par1" in
	dyfi|dnsexit)
	echo "$datetime  ERROR: RegMyIP configuration file "$config_file" missing" | tee -a "$logpath" 
	echo "$datetime  Please run  \"regmyip $service_provider --configure\" to create it" | tee -a "$logpath" 
	echo "$datetime  RegMyIP DDNS client $client_version error exit" | tee -a "$logpath" 
	exit 1
	;;
	*)
	echo "$datetime  ERROR: Invalid service provider $par1 for RegMyIP" | tee -a "$logpath" 
	echo "$datetime  Service providers \"DnsExit\" and \"dy.fi\" supported so far" | tee -a "$logpath"
	echo "$datetime  For others and custom coding work please contact author" | tee -a "$logpath"
	echo "$datetime  RegMyIP DDNS client $client_version error exit" | tee -a "$logpath" 
	exit 1
	;;
	esac
	fi
}

load_id_file()
{
# Verification and loading of ID file
regmyipkey=$(ls -l /dev/disk/by-uuid | grep -e "sda1" -e "hda1" | tr -d '-'| grep -Ewo '[[:alnum:]]{32}'|sha256sum | tr -d ' -')
if [ -z "$regmyipkey" ]; then 
	echo "$datetime  ERROR: Unusual o/s response or something unsupported, giving up. " | tee -a "$logpath"
	echo "$datetime  RegMyIP DDNS client $client_version startup error exit" | tee -a "$logpath"
	exit 1
fi
export regmyipkey
if [ -f "$id_file" ]; then
	user_pw=$(cat "$id_file")
	array_user_pw=($user_pw)
	my_login=$(echo ${array_user_pw[0]} | openssl enc -d -a -aes-256-cbc -pass env:regmyipkey -pbkdf2) 
	my_pw=$(echo ${array_user_pw[1]} | openssl enc -d -a -aes-256-cbc -pass env:regmyipkey -pbkdf2)
else
	echo "$datetime  ERROR: RegMyIP ID file "$id_file" missing" | tee -a "$logpath" 
	echo "$datetime  Please run  \"regmyip <service_provider_name> --configure\" to create it" | tee -a "$logpath" 
	echo "$datetime  RegMyIP DDNS client $client_version error exit" | tee -a "$logpath" 
	exit 1
fi
}

only_yn()
# Accept only y or n answer
{
while :; do
echo -ne "\r                                                \r$1"
read -n1 answer
	case "$answer" in
	y|Y)  echo; break ;;
	n|N)  echo; break ;;
	*)    continue ;; 
	esac
done
}

create_date_dir()
{
# Create date directory and check that we can write there
mkdir -p "$date_path"
if  ! [ -e "$date_path" ]; then
echo "$datetime  Could not create date directory $date_path" | tee -a  "$logpath"
echo "$datetime  RegMyIP DDNS client $client_version error exit" | tee -a "$logpath" 
exit 1
fi
touch "$date_path/00000000"
if  ! [ -e "$date_path/00000000" ]; then
echo "$datetime  Could not write to date directory $date_path" | tee -a  "$logpath"
echo "$datetime  RegMyIP DDNS client $client_version error exit" | tee -a "$logpath" 
exit 1
else
rm "$date_path/00000000"
fi
}

build_config_file()
{
clear
echo

cat << "HERE"

About "Company":
  Some DDNS companies require you have some further identification when 
  DDNS client contacts server. That can be your company or contact 
  name/telephone number for example. That information along with DDNS 
  client name/version is passed to server as UA string and will help 
  technical support staff in case of technical problem.

HERE

get_quoted_text "my_company" "$service_provider.conf"
edit "My identification is: " "$result"
my_company="$result"

get_quoted_text "my_domains" "$service_provider.conf"
edit "Domains (use $separator between domains): " "$result"
my_domains="$result"


cat << "HERE"

About "Deny private route":
  It is technically possible to register only public ip address to DDNS.
  Does this host have public ip address or are you behind NAT ?
  It is a good idea to deny private route when you have public ip address
  (and another local route) in order to avoid false registration via local
  route should public route ever go down. 
  However, you must allow local route when you have private ip address only.
  In that case public address of your router will be registered instead.

HERE

only_yn "Deny private route y/n: "
private=$answer

conf_menu_2()
{
cat << "HERE"

About "ip echo website":
  There are many websites which tell you back only your ip address when
  you go to see the site by web browser.  Best ones reply by bare ip address,
  worse ones also push some adds.  RegMyIP is able to catch ip address even
  when it's embedded among other text and ads --- just in case.
  
Select your ip echo website:

  1.   http://checkip.dy.fi
         (limited for Finland only)
  2.   http://icanhazip.com
         (generic)
  3.   http://myip.dnsomatic.com
         (generic)
  4.   http://checkip.dyndns.com
         (limitation: at least 10 min between requests)

HERE
}

conf_menu_2
while :; do
echo -ne "\r          \rSelect: "
read -n1 task
case $task in
	1)  query_ip_website="http://checkip.dy.fi"; echo -e "\n"; break ;;

	2)  query_ip_website="http://icanhazip.com"; echo -e "\n"; break ;;
	
	3)  query_ip_website="http://myip.dnsomatic.com"; echo -e "\n"; break ;;
	
	4)  query_ip_website="http://checkip.dyndns.com"; echo -e "\n"; break ;;
	
	*)  continue ;;
esac
done

cat << HERE

Verify information:
  Company: $my_company
  Domains: $my_domains
  Deny private route: $private
  Ip echo: $query_ip_website
HERE
only_yn "Is that ok y/n: "
if [ "$answer" = "y" ]; then
	cat > "$config_file" <<- "HERE"
	logpath=$my_path/"$par1".log        # Log is here
	save_ip_path=$my_path/"$par1".myip  # Latest ip address saved here
	date_path=$my_path/.updated/"$par1" # Path of latest update ok files
	dir_http_responses=$my_path         # keep response files here
	refresh_time=432000             # 432000 s = 5 days, 0 (zero) = no refresh
	my_ua="$my_company - $client_name - $client_version $client_release"
	HERE
	echo "my_company=\"$my_company\"" >> "$config_file"
	echo "my_domains=\"$my_domains\"" >> "$config_file"
	if [ "$private" = "n" ]; then
	echo "deny_private_route=false" >> "$config_file"
	else
	echo "deny_private_route=true" >> "$config_file"
	fi
	echo "query_ip_website=\"$query_ip_website\"" >> "$config_file"
	echo "$datetime  Configuration file $config_file created" | tee -a "$logpath" 
else
	echo "$datetime  Editing configuration cancelled, no changes done" | tee -a "$logpath" 
fi
}

build_id_file()
{
clear
regmyipkey=$(ls -l /dev/disk/by-uuid 2>/dev/null | grep -e "sda1" -e "hda1" | tr -d '-'| grep -Ewo '[[:alnum:]]{32}'|sha256sum | tr -d ' -')
if [ -z "$regmyipkey" ]; then 
echo "$datetime  ERROR: Unusual o/s response or something unsupported, giving up. " | tee -a "$logpath"
echo "$datetime  RegMyIP DDNS client $client_version configuration error exit" | tee -a "$logpath"
exit 1
fi
export regmyipkey
cat << "HERE"
About user ID and password:
  Your used ID and password are encrypted before saving to ID file. 
  The key for decrypting that file is not stored anywhere during the process.
  You can use the ID file only on the computer where it was built. 

HERE
read -p "Enter your DDNS user ID: " user_id
read -sp  "Enter your DDNS password: " passwd
echo
id_file=$my_path/"$service_provider".id
echo "$user_id" | openssl  enc -e -a -aes-256-cbc -salt -pass env:regmyipkey -pbkdf2 > "$id_file"
echo "$passwd" | openssl  enc -e -a -aes-256-cbc -salt -pass env:regmyipkey -pbkdf2 >> "$id_file"
if [ -e "$id_file" ]; then
if [ x$user_id = 'x' ] || [ x$passwd = 'x' ]; then
	echo "$datetime  WARNING: Blank user ID or password !"
fi
echo "$datetime  ID file $id_file created for $service_provider" | tee -a "$logpath" 
else
echo "$datetime  Failed to create ID file for $service_provider" | tee -a "$logpath" 
fi
}

configure()
{
echo "$datetime  Client configuration started for $service_provider" >> "$logpath" 
create_date_dir
# While version update there may be old .myip file 
mv "$my_path/$service_provider".myip "$my_path/$service_provider".myip.old 2>/dev/null
echo
conf_menu_1()
{
clear
cat << HERE
¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤
¤     RegMyIP DDNS client $client_version setup utility      ¤  
¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤

HERE

echo "Service provider: $service_provider"

cat <<HERE

Select your task:

  1.   Create or modify configuration file  $service_provider.conf
       (your telephone number, domains, route)
       
  2.   Create ID file             $service_provider.id
       (username and password for $service_provider)

  Q/q  Quit

HERE
}

conf_menu_1
while :; do
echo -ne "\r          \rSelect: "
read -n1 task

case $task in
	1)  build_config_file; conf_menu_1 ;;
	
	2)  build_id_file; conf_menu_1 ;;
	
	q|Q) echo -e "\n"; break ;;
esac
done
exit  # Client must be restarted to load correct configuration
}

help()
{
cat << HERE

RegMyIP $client_version --- easy and secure DDNS Client for crontab

Supported DDNS service providers are:
    http://dnsexit.com (generic)
    http://dy.fi    (Finland only)
Please register there first to get account (free). 

Usage:     regmyip  <service_provider>  -c | --configure
           regmyip  <service_provider>  -l | --log_all
           regmyip  -h | --help

           <service_provider> = dyfi | dnsexit
           
How-to:    Create a directory ddnsclient, copy regmyip there, 
           chmod +x regmyip, run configuration. For example :
           
               ./regmyip dyfi -c 
               ./regmyip dnsexit -c

           Create both configuration and ID file.
            
           Test on command line. Examples:
           
               ./regmyip dyfi -l
           
               ./regmyip dnsexit -l
           
           You will see registration server responses.
           
           Finally create a line to your crontab to run it 
           at reboot and each 10 minutes:
          
               @reboot  sleep 60; <path>/regmyip dyfi --log_all
               */10 * * * * <path>/regmyip dyfi

Security:  User information is encrypted during configuration using
           a key which is also encrypted. ID file is valid only on
           computer where it has been created.  

HERE
}

version()
{
echo "$client_name $client_version $client_release"
}


########################################################################
### Main ###############################################################
########################################################################
# Initial variables some of which will be overdriven by config file
# unless we are doing configuration run
########################################################################
par1="$1"
par2="$2"
pushd $(dirname $0) >/dev/null
my_path=$(pwd)
popd >/dev/null
config_file="$my_path/$par1".conf
id_file="$my_path/$par1".id
logpath="$my_path"/init.log		# Log is here until overdriven from .conf
save_ip_path="$my_path"/init.myip	# (should not appear)
date_path="$my_path"/.updated/"$par1" # Path of latest update ok files
dir_http_responses="$my_path"		# keep response files here
refresh_time=432000			# 432000 s = 5 days, 0 (zero) = no refresh
attempts_max=3				# let's try this many times to connect
a_moment=20					# seconds to wait after each failure
my_ua="$my_company - $client_name - $client_version $client_release"
codelist='nochg|good|abuse|badagent|badauth|badip|badsys|dnserr|!donator|nohost|notfqdn|numhost|!yours|911'
separator=';'				# separator character for domain list
extra_logpath="/dev/null"	# overdriven by command line if wanted
datetime=$(date "+%F_%T")
########################################################################


fault1=false; fault2=false; fault3=false

# We need "curl":
if ! $(which curl &>/dev/null); then 
	echo
	echo "$datetime Prerequisite missing, please install \"curl\" first. " | tee -a "$logpath" 
	fault1=true
fi

# Which DDNS service provider name is accepted and support of SMMD mode:
extra_logpath="/dev/null"
if [ ! "x$par1" = 'x' ]; then
	case "$par1" in 
	dyfi)
		service_provider="dyfi"; smmd=false ;;
	dnsexit)
		service_provider="dnsexit"; smmd=true  ;;
	-h|--help)
		help; exit ;;
	-v|--version)
		version; exit ;;
	*)				
		echo "$datetime  Service provider $par1 not supported" | tee -a "$logpath"
		fault2=true  ;;
	esac
else
	help 
	exit 1
fi	

########################################################################
# Verification and loading of configuration file
# Verification of supported service provider
# Loading bypassed only while running configuration (using initial variables)

if ! [[  "$par2" = "-c" || "$par2" = "--configure" ]]; then
load_config_file
load_id_file
echo "$datetime  RegMyIP DDNS client $client_version config loading done" | tee -a  "$extra_logpath"
fi

#################################################################
# At this point we have variables set from .conf file
# (unless doing --configuration) and we can log to normal logfile
#################################################################

# Second command line parameter checking:
extra_logpath="/dev/null"
if [ ! "x$par2" = 'x' ]; then
	case "$par2" in 
	--configure|-c)
		configure ;;
	--log_all|-l)
		extra_logpath="$logpath"  ;;
	*)				
		echo "$datetime  Invalid command line parameter $par2" | tee -a  "$logpath" 
		fault3=true ;;
	esac
fi

# Exit if anything failed:
if $fault1 || $fault2 || $fault3; then
	echo
	echo "$datetime  RegMyIP DDNS client $client_version error exit" | tee -a "$logpath" 
exit 1
fi

# Verify that hidden dir exists
# This error should never happen unless user has manually deleted the path
if [ ! -d "$my_path"/.updated ]; then
echo "$datetime  Please run configuration, RegMyIP DDNS client $client_version error exit" | tee -a "$logpath"
exit 1
fi

# NAT? #################################################################
# Are we behind NAT or not?
# 1) Is our default gw pointing to local address?
# 2) If yes, do we still have public ip on some network interface?
behind_nat=false
get_our_default_gw  #May return multiple gateways in gw_list

our_gw_ip="$(cut -d ' ' -f1 <<< "$gw_list")"
nic_id="$(cut -d ' ' -f2 <<< "$gw_list")"

any_public_ip "$gw_list"  #Any public ip?

if [ -z "$public_ip" ]; then
echo "$datetime  Notice: No public ip address found at gw address" | tee -a  "$extra_logpath"
#No public ip was found at gw addresses, possibly behind NAT?
#Public ip on any network interface?
	my_ip4_address_list=$(hostname -I | tr ' ' '\n'|grep [.])
	any_public_ip "$my_ip4_address_list"	
	if [ -z "$public_ip" ]; then
		behind_nat=true
		echo "$datetime  Notice: No public ip address found at network interface(s)" | tee -a  "$extra_logpath"
		echo "$datetime  Notice: Behind NAT situation detected" | tee -a  "$extra_logpath"
	else
		echo "$datetime  Notice: Public ip $public_ip found while gw is pointing to private ip address $our_gw_ip" | tee -a  "$extra_logpath"	
	fi	
fi

#### DEBUG block ###############
#echo "1 $public_ip"           #
#echo "2 $our_gw_ip"           #
#echo "3 $nic_id"              #
#echo "4 $my_ip4_address_list" #
#echo "5 $behind_nat"          #
#read -p STOP                  #
################################

# We are ready to go ahead
# We request our ip from web server
# To make code more robust we retry after curl failure 
success=false
attempts=$attempts_max
until [ $attempts = 0 ]; do
	datetime=$(date "+%F_%T")
	http_response=$(curl --interface $nic_id -k -A"$my_ua" -D - "$query_ip_website" 2>/dev/null )
	result=$?
	echo "$http_response" > "$dir_http_responses/$service_provider.eresp"
	if [ $result = 0 ]; then
		echo "$datetime  Address query using interface $nic_id from $query_ip_website ok" | tee -a  "$extra_logpath"
		attempts=0; success=true
	else
		explain_curl_result $result
		echo "$datetime  Address query using interface $nic_id from $query_ip_website failed, curl error $result $e_txt" | tee -a  "$logpath" 
		echo -e "\nCurl was not able to connect, see error log $logpath\n"
		attempts=$[attempts-1]
		[[ $attempts -gt 0 ]] && for ((i=$a_moment; i>0; i--)); do echo -en "Wait... $i \r"; sleep 1; echo -en "         \r"; done
	fi
done
if [ ! $success ]; then
	echo "$datetime   TIMEOUT: Could not connect using interface $nic_id to $query_ip_website after trying $attempts times using $a_moment seconds pause." | tee -a  "$logpath" 
	extra_action_communication_failure
	echo "$datetime  RegMyIP DDNS client $client_version error exit" | tee -a "$logpath" 
	exit 65
fi

# We attempt to scan for ip address among possible other text in http response:
datetime=$(date "+%F_%T")
ip_web=$(echo $http_response | grep -o -E  '[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}')

if valid_ip "$ip_web"; then
	echo "$datetime  $query_ip_website told my ip address is $ip_web" | tee -a  "$extra_logpath"
else
	echo "$datetime  Address not found from $query_ip_website, error $result" | tee -a "$logpath" 
	echo "$datetime  RegMyIP DDNS client $client_version error exit" | tee -a "$logpath" 
	exit 66
fi

#####################################
# Behind NAT activity?
# Result will be registration of your router public address and NOT your host address
# if that is allowed by "deny_private_route" variable
#Need to decide desirable action if public ip is not available:
if $behind_nat; then
	if "$deny_private_route"; then
	echo "$datetime  Error: No public ip, gateway is $our_gw_ip and deny_private_route is enabled" | tee -a "$logpath" 
	echo "$datetime  No registration attempt will be done" | tee -a "$logpath" 
	echo "$datetime  RegMyIP DDNS client $client_version normal exit" | tee -a "$logpath" 
	exit 69
	else
	echo "$datetime  WARNING: Public ip $ip_web of your router will be registered, NOT your address" | tee -a  "$extra_logpath"
	echo "$datetime  WARNING: Using route via gateway $our_gw_ip interface $nic_id " | tee -a  "$extra_logpath"
	fi
else
	echo "$datetime  IP address $my_ip4_address_list gateway $our_gw_ip interface $nic_id " | tee -a  "$extra_logpath"
fi

###########################################################################
# Valid ip address was found, let's continue
# We compare each domain and build a list of those
# domains which need registration of changed ip address
ip_mismatch=false
unset domains_to_update		# semicolon separated list of strings
# Check for missing ip file or empty date stamp dir (i.e. first run)
if [ ! -e "$save_ip_path" ] || [ $(ls -lA "$date_path/"* &>/dev/null; echo $?) != "0" ]; then
	action_first_time		# We branch out here on the first run
else	# ip file exists
	oldifs="$IFS"
	IFS=';' array_my_domains=(${my_domains[@]})
	for i in "${!array_my_domains[@]}"; do  # we check each our domain ##
		ip_dns=$(host "${array_my_domains[i]}" | grep -o -E  '[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}')
		# we don't get any ip if not registered (NXDOMAIN) i.e. cleared manually at web etc.
		if [ -z $ip_dns ]; then ip_dns="_none_"; fi
		saved_ip=$(cat "$save_ip_path")
		if [ "$ip_dns" != "_none_" ]; then
			if [ "$saved_ip" != "$ip_dns" ]; then
			ip_mismatch=true	# We check next whether that is serious
			fi
		fi
		if [ "$ip_web" != "$ip_dns" ]; then
			if [ "$ip_mismatch" = "true" ]; then mismatch_warning; fi
			append_string domains_to_update "${array_my_domains[i]}" "separator"
		else	
			echo $ip_web > "$save_ip_path"  # Sync local file
		fi
	done  ###################### each domain checked one by one ########
	IFS="$oldifs"
	# Now we have a list of domains in "domains_to_update"

	if [ -z "$domains_to_update" ]; then
	action_no_change
	else
	action_changed
	fi
fi

# Finally we check how long time has passed since latest successful
# update and refresh registration before expiry. 
# Some service providers will expire registration after 7 days if client 
# does not keep registration valid by doing refresh before that.
# Our default is to do refresh each 5 days.
expiry_avoidance
echo "$datetime  RegMyIP DDNS client $client_version normal exit" | tee -a  "$extra_logpath"

