#!/bin/bash
#
# Simple shell script to update IP2Location database files
#
# For updating non-sample files, credentials are or token is required:
#  provided by file $HOME/.ip2location
#   credential-based authentication:
#    login=EMAIL
#    password=PASSWORD
#   token-based authentication:
#    token=TOKEN
#
# Project    : ipv6calc/IP2Location
# File       : IP2Location-update.sh
# Copyright  : 2012-2025 by Peter Bieringer <pb (at) bieringer.de>
# License    : GNU GPL version 2

# 20150416: remove softlink support, no longer needed
# 20210528: add support for 'token'
# 20240124: add optional -R|t support
# 20250128: add support for MMDB databases
# 20250615: fix sample download URL, extend support for ASN files, fix error detection on unzip
# 20250913: unconditionally display download URL in verbose mode

IP2LOCATION_CONFIG=${IP2LOCATION_CONFIG:-"$HOME/.ip2location"}

IP2LOCATION_DAT_DIR_DEFAULT="/usr/share/IP2Location"
[ -z "$IP2LOCATION_DAT_DIR" -a -n "$IPV6CALC_DB_IP2LOCATION_DIR" ] && IP2LOCATION_DAT_DIR="$IPV6CALC_DB_IP2LOCATION_DIR"
[ -z "$IP2LOCATION_DAT_DIR" ] && IP2LOCATION_DAT_DIR="$IP2LOCATION_DAT_DIR_DEFAULT"

IP2LOCATION_DAT_URL_BASE="https://www.ip2location.com/download?password=@PASSWORD@&token=@TOKEN@&login=@LOGIN@"
IP2LOCATION_DAT_URL_BASE_SAMPLE="https://cdn.ip2location.com/samples"
IP2LOCATION_DAT_URL_BASE_LITE_FREE="https://download.ip2location.com/lite"

# Format: Proto:DB-Number

# free downloads
IP2LOCATION_DAT_FILES_LITE_FREE=${IP2LOCATION_DAT_FILES_LITE_FREE:-"IPv4:1 IPv6:1"}
IP2LOCATION_MMDB_FILES_LITE_FREE=${IP2LOCATION_MMDB_FILES_LITE_FREE:-"IP:1"}

# (free) subscription is required for LITE DB-Number > 1
IP2LOCATION_DAT_FILES_LITE=${IP2LOCATION_DAT_FILES_LITE:-"IPv4:11 IPv6:11 IPV4:ASN IPv6:ASN"}
IP2LOCATION_MMDB_FILES_LITE=${IP2LOCATION_MMDB_FILES_LITE:-"IP:ASN"}

# list of normal (purchased subscription required)
IP2LOCATION_DAT_FILES="${IP2LOCATION_DAT_FILES}"
IP2LOCATION_MMDB_FILES="${IP2LOCATION_MMDB_FILES}"

# list of recommended SAMPLE
IP2LOCATION_DAT_FILES_SAMPLE=${IP2LOCATION_DAT_FILES_SAMPLE:-"IPv4:26 IPv6:26"}
IP2LOCATION_MMDB_FILES_SAMPLE=${IP2LOCATION_MMDB_FILES_SAMPLE:-""}

# optional ipv6calc-db-update-support.sh
supportfile="$(dirname "$0")/ipv6calc-db-update-support.sh"
if [ -r "$supportfile" ]; then
	echo "INFO  : found and sourced: $supportfile"
	source $supportfile
fi

help() {
	cat <<END
Usage: $(basename "$0") [-D <dir>] [-n] [-s] [-T] [-v] ${have_ipv6calc_db_update_support:+[-R [-r]]}
	-D <dir>	database destination directory (optional)
	-n		no action (dry-run)
	-s		skip already successfully downloaded files
	-T		enforce use of 'token' instead of using 'login' & 'password'
	-v		verbose
	-S <selector>	download only sample file given by selector IPv4:<num> or IPv6:<num>
END

	[ -n "$have_ipv6calc_db_update_support" ] && ipv6calc_db_update_support_help

	cat <<END

	database directory: $IP2LOCATION_DAT_DIR (default: $IP2LOCATION_DAT_DIR_DEFAULT)

	it honors externally defined environment value:
		prio 1: IP2LOCATION_DAT_DIR
		prio 2: IPV6CALC_DB_IP2LOCATION_DIR

 this script will download data from ip2location.com
  into given/set database directory

 Sample databases:
  IP2LOCATION_DAT_FILES_SAMPLE="$IP2LOCATION_DAT_FILES_SAMPLE"
  IP2LOCATION_MMDB_FILES_SAMPLE="$IP2LOCATION_MMDB_FILES_SAMPLE"

 Lite (free) databases:
  IP2LOCATION_DAT_FILES_LITE_FREE="$IP2LOCATION_DAT_FILES_LITE_FREE"
  IP2LOCATION_MMDB_FILES_LITE_FREE="$IP2LOCATION_MMDB_FILES_LITE_FREE"

 With authentication (requires login/password or token in $IP2LOCATION_CONFIG) and a valid subscription
  IP2LOCATION_DAT_FILES_LITE="$IP2LOCATION_DAT_FILES_LITE"
  IP2LOCATION_DAT_FILES="$IP2LOCATION_DAT_FILES"
  IP2LOCATION_MMDB_FILES_LITE="$IP2LOCATION_MMDB_FILES_LITE"
  IP2LOCATION_MMDB_FILES="$IP2LOCATION_MMDB_FILES"

 Credentials or token must be defined in \$HOME/.ip2location by
  login=IP2LOCATION-LOGIN       ${login:+found: ${login:0:2}...}
  password=IP2LOCATION-PASSWORD ${password:+found: ${password:0:2}...}
  token=IP2LOCATION-TOKEN       ${token:+found: ${token:0:2}...}

 In addition settings from above can be overwritten by setting related variables
END
}

# source credentials (must provide login= and password= or token=)
authenticated=false
has_token=false
has_credentials=false
verbose=false

if [ -f "$IP2LOCATION_CONFIG" ]; then
	source "$IP2LOCATION_CONFIG" || exit 1

	if [ -n "${token}" ]; then
		has_token=true
		authenticated=true
		echo "INFO  : found token in: $IP2LOCATION_CONFIG (for authenticated downloads)"
	fi

	if [ -n "${login}" -a -n "${password}" ]; then
		has_credentials=true
		authenticated=true
		echo "INFO  : found credentials in: $IP2LOCATION_CONFIG (for authenticated downloads)"
	fi

	if ! $authenticated; then
		echo "NOTICE: missing credentials/token in: $IP2LOCATION_CONFIG (skip authenticated downloads)"
	fi
fi

# option parsing
while getopts "vfsntRrS:D:h\?" opt; do
	case $opt in
	    D)
		IP2LOCATION_DAT_DIR=$OPTARG
		;;
	    n)
		NO_ACTION=1
		testrun=1
		;;
	    s)
		SKIP_OK=1
		;;
	    S)
		# clear defaults
		IP2LOCATION_DAT_FILES=""
		IP2LOCATION_MMDB_FILES=""
		IP2LOCATION_DAT_FILES_LITE_FREE=""
		IP2LOCATION_MMDB_FILES_LITE_FREE=""
		IP2LOCATION_DAT_FILES_LITE=""
		IP2LOCATION_MMDB_FILES_LITE=""
		IP2LOCATION_DAT_FILES_SAMPLE=""
		IP2LOCATION_MMDB_FILES_SAMPLE=""

		#  use option
		IP2LOCATION_DAT_FILES_SAMPLE="$OPTARG"
		;;
	    t)
		has_credentials=false
		;;
	    v)
		verbose=true
		;;
	    R)
		if [ -z "$have_ipv6calc_db_update_support" ]; then
			echo "WARN  : -R is not supported as extension was not loaded: ipv6calc-db-update-support.sh"
		else
			reloadrestart=1
		fi
		;;
	    r)
		testrun=1
		;;
	    *)
		help
		exit 1
		;;
	esac
done


# directory check
if [ ! -d "$IP2LOCATION_DAT_DIR" ]; then
	echo "ERROR : missing directory: $IP2LOCATION_DAT_DIR (check option -D or set IP2LOCATION_DAT_DIR)"
	exit 1
fi

if [ ! -w "$IP2LOCATION_DAT_DIR" ]; then
	echo "ERROR : missing write permissions on directory: $IP2LOCATION_DAT_DIR"
	exit 1
fi

# required binary check
if ! which unzip >/dev/null 2>&1; then
	echo "ERROR : missing binary: unzip"
	exit 1
fi

if ! $authenticated; then
	echo "NOTICE: IP2Location credentials/token not found, clear IP2LOCATION_*_FILES"
	IP2LOCATION_DAT_FILES=""
	IP2LOCATION_MMDB_FILES=""

	echo "NOTICE: IP2Location credentials/token not found, clear IP2LOCATION_*_FILES_LITE"
	IP2LOCATION_DAT_FILES_LITE=""
	IP2LOCATION_MMDB_FILES_LITE=""
fi

if $has_token; then
	# replace placeholders
	IP2LOCATION_DAT_URL_BASE=${IP2LOCATION_DAT_URL_BASE//@TOKEN@/$token}
	# clear credential parameters
	IP2LOCATION_DAT_URL_BASE=${IP2LOCATION_DAT_URL_BASE//@LOGIN@/}
	IP2LOCATION_DAT_URL_BASE=${IP2LOCATION_DAT_URL_BASE//@PASSWORD@/}
elif $has_credentials; then
	# replace placeholders
	IP2LOCATION_DAT_URL_BASE=${IP2LOCATION_DAT_URL_BASE//@LOGIN@/$login}
	IP2LOCATION_DAT_URL_BASE=${IP2LOCATION_DAT_URL_BASE//@PASSWORD@/$password}
	# clear token parameter
	IP2LOCATION_DAT_URL_BASE=${IP2LOCATION_DAT_URL_BASE//@TOKEN@/}
fi

# Download and unpack non-sample files
error=0
for list in IP2LOCATION_DAT_FILES_LITE IP2LOCATION_DAT_FILES IP2LOCATION_DAT_FILES_LITE_FREE IP2LOCATION_DAT_FILES_SAMPLE IP2LOCATION_MMDB_FILES_LITE IP2LOCATION_MMDB_FILES IP2LOCATION_MMDB_FILES_LITE_FREE IP2LOCATION_MMDB_FILES_SAMPLE; do
	if [ -z "${!list}" ]; then
		echo "NOTICE: empty list, skip: $list"
		continue
	fi

	$verbose && echo "INFO  : download list $list: ${!list}"

	url_base="$IP2LOCATION_DAT_URL_BASE"

	case $list in
	    *_LITE|*_LITE_FREE)
		code_suffix="BIN"
		suffix="BIN"
		file_prefix="IP2LOCATION-LITE"

		case $list in
		    *_LITE_FREE)
			url_base="$IP2LOCATION_DAT_URL_BASE_LITE_FREE"
			;;
		esac
		;;
	    *_SAMPLE)
		url_base="$IP2LOCATION_DAT_URL_BASE_SAMPLE"
		suffix="BIN"
		;;
	    *)
		code_suffix="BIN"
		suffix="BIN"
		file_prefix="IP2LOCATION"
		;;
	esac

	case $list in
	    *_MMDB_*)
		code_suffix="MMDB"
		suffix="MMDB"
		;;
	esac

	case $list in
	    *_LITE|*_LITE_FREE)
		code_suffix="LITE${code_suffix}"
		;;
	esac

	file_dest="$IP2LOCATION_DAT_DIR/$file"

	if [ "${!list}" = "skip" ]; then
		echo "NOTICE: skip list (by keyword 'skip'): $list"
		continue
	fi

	for item in ${!list}; do
		proto=${item/:*}
		number=${item/*:}

		case $list in
		    *_SAMPLE)
			case $proto in
			    IPv4)
				protoitem=""
				;;
			    IPv6)
				protoitem="6"
				;;
			    *)
				echo "ERROR : unsupported 'proto' in item: $item"
				exit 1
				;;
			esac

			file="sample${protoitem}.bin.db${number}.zip"
			;;
		    *)
			case $proto in
			    IPv4|IP)
				if [ ${number} = "ASN" ]; then
					code="DB${number}${code_suffix}"
					file="${file_prefix}-${number}.$suffix.ZIP"
				else
					code="DB${number}${code_suffix}"
					file="${file_prefix}-DB${number}.$suffix.ZIP"
				fi
				;;
			    IPv6)
				if [ ${number} = "ASN" ]; then
					code="DB${number}${code_suffix}IPV6"
					file="${file_prefix}-${number}.IPV6.$suffix.ZIP"
				else
					code="DB${number}${code_suffix}IPV6"
					file="${file_prefix}-DB${number}.IPV6.$suffix.ZIP"
				fi
				;;
			    *)
				echo "ERROR : unsupported 'proto' in item: $item"
				exit 1
				;;
			esac

			;;
		esac

		case $list in
		    *_SAMPLE|*_LITE_FREE)
			url="$url_base/$file"
			;;
		    *)
			url="$url_base&productcode=$code"
			;;
		esac

		file_dest="$IP2LOCATION_DAT_DIR/$file"

		url_masked="$url"
		re="(.*password=)([^&]+)(&.*)"
		if [[ $url_masked =~ $re ]]; then
			url_masked=${BASH_REMATCH[1]}**PASS**${BASH_REMATCH[3]}
		fi
		re="(.*token=)([^&]+)(&.*)"
		if [[ $url_masked =~ $re ]]; then
			url_masked=${BASH_REMATCH[1]}**TOK**${BASH_REMATCH[3]}
		fi

		if [ "$NO_ACTION" = "1" ]; then
			if $has_credentials; then
				echo "NOTICE: download skipped by option: ${url_masked} ($file_dest)"
				continue
			fi
			if $has_token; then
				echo "NOTICE: download skipped by option: ${url_masked} ($file_dest)"
				continue
			fi
		fi

		if [ "$SKIP_OK" = "1" ]; then
			if file "$file_dest" | grep -q "Zip archive data"; then
				echo "NOTICE: download skipped by option because file already existing and is a ZIP file: $code ($file_dest)"

				file_unzip=$(unzip -l "$file_dest" "*.$suffix" | grep "\.$suffix$" | awk '{ print $NF }' | head -1)

				if [ -n "$file_unzip" ]; then
					if [ ! -f "$IP2LOCATION_DAT_DIR/$file_unzip" ]; then
						unzip -o -d "$IP2LOCATION_DAT_DIR" "$file_dest" "*.$suffix"
						if [ $? -ne 0 ]; then
							echo "ERROR : unzip of file not successful: $file_dest"
							error=$[ $error + 1 ]
							continue
						else
							echo "INFO  : unzip of file successful: $file_dest"
						fi
					else
						echo "INFO  : skip unzip because unzipped file already existing: $code ($file_dest)"
					fi
				fi
				continue
			fi
		fi

		$verbose && echo "INFO  : try to download URL: ${url_masked} ($file_dest)"

		wget -q -O "$file_dest" "$url"
		result=$?
			
		if [ $result -ne 0 ]; then
			echo "ERROR : download of file not successful: $file_dest ($!)"
			error=$[ $error + 1 ]
			continue
		fi
		$verbose && echo "INFO  : download of file successful: $file_dest"

		if [ ! -s "$file_dest" ]; then
			echo "ERROR : downloaded file has ZERO size: $file_dest"
			error=$[ $error + 1 ]
			continue
		fi

		if ! file "$file_dest" | grep -q "Zip archive data"; then
			if file "$file_dest" | grep -q "ASCII text"; then
				echo "ERROR : downloaded file is not a ZIP archive: $file_dest ($(head -1 "$file_dest"))"
				error=$[ $error + 1 ]
			else
				echo "ERROR : downloaded file is not a ZIP archive: $file_dest"
				error=$[ $error + 1 ]
			fi
			continue
		fi

		unzip -o -d "$IP2LOCATION_DAT_DIR" "$file_dest" "*.$suffix"
		if [ $? -ne 0 ]; then
			echo "ERROR : unzip of file not successful: $file_dest"
			error=$[ $error + 1 ]
			continue
		fi
		$verbose && echo "INFO  : unzip of file successful: $file_dest (remove downloaded file now)"
		rm -f "$file_dest"

		echo "NOTICE: download/unzip successful: $item"
	done
done

# Adjust permissions
if [ "$NO_ACTION" != "1" ]; then
	chmod 644 $IP2LOCATION_DAT_DIR/*.BIN
	chmod 644 $IP2LOCATION_DAT_DIR/*.MMDB
fi

# calling external function
if [ -n "$have_ipv6calc_db_update_support" ]; then
	[ "$reloadrestart" = "1" ] && ipv6calc_db_update_support_reloadrestart "$IP2LOCATION_DAT_DIR" "$testrun"
fi

if [ $error -gt 0 ]; then
	echo "WARN  : $errors ERRORS occurred during execution"
else
	echo "INFO  : NO errors occurred during execution"
fi
