#!/bin/sh
##############################################
SETUP_FILE_CURRENT='setup.tar.xz'

current_setup_file='/usr/local/etc/telem/setup.tar.xz'
new_setup_file='/usr/local/etc/telem/setup.new.tar.xz'
revert_setup_file='/usr/local/etc/telem/setup.revert.tar.xz'
#reload_setup_file='/usr/local/etc/telem/setup.reload.tar.xz'

SDDConfig='/mnt/sd/config'
current_sdd_config="${SDDConfig}/setup.tar.xz"
new_sdd_setup="/mnt/sd/setup.new.tar.xz"

#I2C_PROBLEM_FILE='/var/local/telem/errors/i2c-bus'

# created by telem
clear_setup_on_boot_file='/var/local/telem/clear_setup_on_boot'
# created by S99
reload_setup_on_boot_file='/var/local/telem/reload_setup_on_boot'

default_setup_dir=
DEFAULT_SETUP_FILE='/etc/default/setup/setup.tar.xz'
DEFAULT_USER_SETUP_FILE='/etc/default/user_setup/setup.tar.xz'
##############################################
filterPath='/usr/local/bin/gwconf-files/filters'
converterPath='/usr/local/bin/gwconf-files/converters'
systemDefaultIsCorrect=true
tmpBaseName='gwconf'
gw_doFix=true
gw_doTest=true
gw_verbose=false
gw_rmUnknown=false
##############################################
isRootOnly() {
	# check if file is not writable by non-root
	local fpath="${1}"
	[ -f "${fpath}" ] || return 1
	ls -n "${fpath}" | awk '{print $1,$3,$4,$9}' | \
	grep -e "-.......-- 0 0 ${fpath}" -e "-....--.-- 0 .\+ ${fpath}" &>/dev/null
}
##############################################
if isRootOnly '/usr/local/etc/gwconf.conf'; then
	. /usr/local/etc/gwconf.conf
fi
##############################################

gw_log() {
	[ -n "${1}" ] && printf -- "${1}\n"
}

gw_log_verbose() {
	${gw_verbose} && gw_log "${1}" || true
}

gw_diff() {
	diff -U 0 "$@"
}

gw_rm() {
	rm "$@"
}

getConfPath() {
	# $1: path or string
	# allow only *.tar.xz files or any of predefined ID-s
	case "$1" in
		[0-9])
			echo "${current_setup_file}.${1}"
		;;
		backup[0-9]|old[0-9])
			echo "${current_setup_file}.$(echo "${1}" | sed 's|backup||g;s|old||g')"
		;;
		c|cur|current)
			echo "${current_setup_file}"
		;;
		d|def|default)
			[ -f "${DEFAULT_USER_SETUP_FILE}" ] && \
			echo "${DEFAULT_USER_SETUP_FILE}"   || \
			echo "${DEFAULT_SETUP_FILE}"
		;;
		n|new)
			echo "${new_setup_file}"
		;;
		s|sdef|system-default)
			echo "${DEFAULT_SETUP_FILE}"
		;;
		u|udef|user-default)
			echo "${DEFAULT_USER_SETUP_FILE}"
		;;
		*)
			# /home/*/*.tar.xz
			# /root/*.tar.xz
			if echo "${1%/*}" | grep -q -- '^/'; then
				echo "${1}"
			else
				# Convert to absolute path
				echo "$(pwd)/${1}"
			fi | awk '/^\/(home\/[a-z_][a-z0-9_-]*|root)\/[A-Za-z0-9_.-]*.tar.xz$/'
		;;
	esac
}

cpSourceFilter() {
	# $1: file path to test for gwconf-cp
	"${filterPath}/gwconf-cp.awk" -v s=1
}

cpTargetFilter() {
	# $1: file path to test for gwconf-cp
	"${filterPath}/gwconf-cp.awk" -v t=1
}

isSystemDefault() {
	# $1: archive path
	[ "${1}" = "${DEFAULT_SETUP_FILE}" ] && return 0
	[ -r "${1}" ] && [ -r "${DEFAULT_SETUP_FILE}" ] || return 1
	diff -q "${1}" "${DEFAULT_SETUP_FILE}" &>/dev/null
}

isUserDefault() {
	# $1: archive path
	# Usually user default is not present
	[ "${1}" = "${DEFAULT_USER_SETUP_FILE}" ] && return 0
	[ -r "${1}" ] && [ -r "${DEFAULT_USER_SETUP_FILE}" ] || return 1
	diff -q "${1}" "${DEFAULT_USER_SETUP_FILE}" &>/dev/null
}

isDefaultSetup() {
	# $1: archive path
	isSystemDefault "${1}" || isUserDefault "${1}"
}

maybeDefaultSetup() {
	# $1: tar.xz or directory with extracted content
	# test if it is default setup

	# file
	[ "${1}" = "${DEFAULT_SETUP_FILE}"      ] && return 0
	[ "${1}" = "${DEFAULT_USER_SETUP_FILE}" ] && return 0

	# directory (only for system default)
	# common system default content
	[ -d "${1}/etc"        ] && \
	[ -f "${1}/md5sums"    ] && \
	[ -f "${1}/setup_info" ] && \
	return 0

	# not default setup
	return 1
}

readGWSConf() {
	# $1: tar.xz or directory with extracted content
	# $2: awk pattern
	# ex: readGWSConf setup.tar.xz  Firewall.mode
	local f='GWS/gws.conf.txt'
	(
		set -o pipefail
		if [ -f "${1}/${f}" ]; then
			cat "${1}/${f}"
		elif [ -f "${1}" ]; then
			tar -xOf "${1}" "${f}"   2>/dev/null || \
			tar -xOf "${1}" "./${f}" 2>/dev/null
		else
			return 1
		fi | awk "/${2}/{print \$0;exit}"
	)
}

testcmd() {
	# $1: command name
	command -v "$1" &>/dev/null
}

sevenBytes() {
	# $1: path to file
	if testcmd xxd; then
		xxd -p -l7 "$1"
	else
		# for older releases
		hexdump -e '7/1 "%02x" "\n"' -n7 "$1"
	fi
}

isTarXz() {
	# $1: path to tar.xz
	# check extension and mime type
	echo "$1" | grep -q -- '.*\.tar\.xz$\|\.tar\.xz\.[0-9]$' || return 1
	sevenBytes "$1" | grep -Fq 'fd377a585a0000'
}

getExtractedSize() {
	# $1: path to tar.xz
	(
		set -o pipefail
		xz --robot --list "${1}" 2>/dev/null \
		| awk '/totals/{print $5}'
	)
}

MAX_UNXZ_SIZE=99999999
testConfigArchive() {
	# $1: path to configuration tar.xz
	[ -f "${1}" ]  || { echo "File does not exist"  ; return 1; }
	[ -r "${1}" ]  || { echo "File is not readable" ; return 1; }
	isTarXz "${1}" || { echo "File is not a tar.xz" ; return 1; }
	# check file size
	local size
	size="$(du -k "${1}" | cut -f1)"
	[ "$size" -gt 5000 ] && { echo "Configuration too big"; return 1; }
	# test archive
	size="$(getExtractedSize "${1}")"    || { echo "Could not get extracted size"; return 1; }
	[ "${size}" -gt "${MAX_UNXZ_SIZE}" ] && { echo "Extracted size too big (${size} > ${MAX_UNXZ_SIZE})"; return 1; }
	# TODO: test free space
	return 0
}

extractConfig() {
	# $1: archive path
	# $2: target directory
	xzcat -t "${1}" \
	|| { echo "Bad xz file";     return 1; }
	xzcat "${1}" | tar x -C "${2}" \
	|| { echo "Bad tar.xz file"; return 1; }
}

getAllFiles() {
	# $1: directory with extracted content
	find "${1}" ! -type d -print \
	| sed "s|^${1}||g;/^\/GWS/d" \
	| sort
}

getFileList() {
	# $1: directory with extracted content
	getAllFiles "${1}" | "${filterPath}/GWS-files.awk"
}

getScriptFileList() {
	# $1: directory with extracted content
	getAllFiles "${1}" | "${filterPath}/GWS-files.awk" -v sh=1
}

getSourcedFileList() {
	# $1: directory with extracted content
	getAllFiles "${1}" | "${filterPath}/GWS-files.awk" -v shconf=1
}

getOtherFileList() {
	# $1: directory with extracted content
	getAllFiles "${1}" | "${filterPath}/GWS-files.awk" -v other=1
}

testFileList() {
	# $1: directory with extracted content
	local CHR="${1}"
	local filter="${filterPath}/GWS-files.awk"
	local tmp1="$(mktemp -t ${tmpBaseName}-tmp.XXXXXX)"
	local tmp2="$(mktemp -t ${tmpBaseName}-tmp.XXXXXX)"
	# all files
	getAllFiles "${CHR}"  > "${tmp1}"
	# filter allowed
	"${filter}" "${tmp1}" > "${tmp2}"
	# print difference
	gw_diff -L 'All files' -L 'Allowed files' -- "${tmp1}" "${tmp2}" \
	|| { gw_rm "${tmp1}" "${tmp2}"; return 1; }
	gw_rm "${tmp1}" "${tmp2}"
}

testFilterFile() {
	# $1: filter name
	# $2: file to test
	local filter="${filterPath}/${1}.awk"
	local source="${2}"
	[ -s "${source}" ] || return 0

	local tmp1="$(mktemp -t ${tmpBaseName}-tmp.XXXXXX)"
	"${filter}" "${source}" > "${tmp1}" || return 1
	# filtered file has to be same as source
	gw_diff -L "${source##*/}" -L "filtered with ${1}" -- "${source}" "${tmp1}" \
	|| { gw_rm "${tmp1}"; return 1; }
	gw_rm "${tmp1}"
}

testLogConfXML() {
	# $1: path to log-conf.xml
	[ -f "${1}" ] || return 0
	grep -Fq -- '<log4j:configuration' "${1}" || return 1
	# xmlwf does not set return codes
	local xmlerr="$(xmlwf "${1}")"
	gw_log "${xmlerr}"
	[ -z "${xmlerr}" ]
}

testSetupXML() {
	# $1: path to setup.xml
	[ -f "${1}" ] || return 0
	# xmlwf does not set return codes
	local xmlerr="$(xmlwf "${1}")"
	gw_log "${xmlerr}"
	[ -z "${xmlerr}" ]
}

testTZ() {
	# $1: path to TZ
	[ -f "${1}" ] || return 0
	local tmptz="$(awk 'BEGIN{RS = "\r\n|\n"}/^[A-Za-z0-9.:,\/+-]*$/' "${1}" | head -n1)"
	[ "${tmptz}" = "$(tr -d '\r' < "${1}")" ] || return 1
}

matchFirstLine() {
	# $1: file to test
	# $2: desired string
	if head -1 "${1}" | grep -Fqx -- "${2}"; then
		return 0
	else
		gw_log "Wrong first line in ${1##*/}, it should be <${2}>"
		return 1
	fi
}

hasDOS() {
	# $1: file to test
	cat -v "${1}" | grep -q -- '\^M$'
}

doDos2unix() {
	# $1: file to fix
	[ -f "${1}" ] || return 0
	if command -v dos2unix &>/dev/null; then
		dos2unix "${1}"
	else
		local tmp1="$(mktemp -t ${tmpBaseName}-tmp.XXXXXX)"
		awk 'BEGIN{RS = "\r\n|\n"}//' "${1}" > "${tmp1}"
		mv "${tmp1}" "${1}"
	fi
}

fixDOS() {
	# $1: file to process
	if ${gw_verbose}; then
		local before="$(md5sum "${1}")"
		doDos2unix "${1}"
		[ "${before}" = "$(md5sum "${1}")" ] || gw_log "Removed DOS line endings in ${1##*/}"
	else
		doDos2unix "${1}"
	fi
	return 0
}

testScriptFile() {
	# $1: file to process

	# 1. check shebang
	# 2. check syntax
	# 3. filter

	matchFirstLine "${1}" '#!/bin/sh' || return 1
	sh -o noexec   "${1}"             || return 1
	case "${1##*/}" in
		S39iptables)
			testFilterFile 'S39-script'   "${1}" || return 1
		;;
		S40network|network_eth[1-5])
			testFilterFile 'S40-script'   "${1}" || return 1
		;;
		ipsec-ip-up|ipsec-ip-down)
			testFilterFile 'IPSEC-script' "${1}" || return 1
		;;
		l2tp-ip-up|l2tp-ip-down)
			testFilterFile 'L2TP-script'  "${1}" || return 1
		;;
		*)
			gw_log "NOT TESTED: ${1##*/}"
			return 1
		;;
	esac
	return 0
}

testSourcedFile() {
	# $1: file to process

	# Test
	sh -o noexec "${1}"                  || return 1
	testFilterFile 'sourced-conf' "${1}" || return 1
	# TODO: filter incorrect variable names
}

testOtherFile() {
	#matchFirstLine "${1}" '#!/bin/sh' || return 1
	#sh -o noexec   "${1}"             || return 1
	case "${1##*/}" in
		dns.conf)
			testFilterFile 'dns-conf'    "${1}" || return 1
		;;
		client*.conf)
			testFilterFile 'openvpn-client-conf' "${1}" || return 1
		;;
		*options)
			testFilterFile 'ppp-options' "${1}" || return 1
		;;
		racoon.conf)
			testFilterFile 'racoon-conf' "${1}" || return 1
		;;
		ipsec.conf|swanctl.conf)
			testFilterFile 'ipsec-conf' "${1}" || return 1
		;;
		ptp4l.cfg|ptp4l.*.cfg)
			testFilterFile 'ptp4l-cfg' "${1}" || return 1
		;;
		snmpd.conf)
			testFilterFile 'snmpd-conf' "${1}"  || return 1
		;;
		sshd_config)
			testFilterFile 'ssh-config' "${1}"  || return 1
		;;
		*)
			return 0
		;;
	esac
	return 0
}

testScriptFiles() {
	# $1: directory with extracted content

	for f in $(getScriptFileList "${1}"); do
		[ -f "${1}${f}" ]         || return 1
		testScriptFile "${1}${f}" || return 1
		gw_log_verbose "OK: ${f}"
	done

	for f in $(getSourcedFileList "${1}"); do
		[ -f "${1}${f}" ]          || return 1
		testSourcedFile "${1}${f}" || return 1
		gw_log_verbose "OK: ${f}"
	done

	for f in $(getOtherFileList "${1}"); do
		[ -f "${1}${f}" ]        || return 1
		testOtherFile "${1}${f}" || return 1
		gw_log_verbose "OK: ${f}"
	done
}

testOtherFiles() {
	# $1: directory with extracted content
	local CHR="${1}"

	# check shebang
	local tmp="${CHR}/etc/ipsec-tools.conf"
	if [ -f "${tmp}" ]; then
		matchFirstLine "${tmp}" '#!/usr/sbin/setkey -f' || return 1
	fi

	# check timezone format
	local tmp="${CHR}/etc/TZ"
	testTZ "${tmp}" || return 1

	# test log xml
	local tmp="${CHR}/usr/local/etc/telem/log-conf.xml"
	testLogConfXML "${tmp}" || return 1

	# test setup xml
	local tmp="${CHR}/usr/local/etc/telem/setup.xml"
	testSetupXML "${tmp}" || return 1

	return 0
}

deleteUnknownFiles() {
	# $1: directory with extracted content
	${gw_rmUnknown} || return 0
	testFileList ${1} | awk '/^-\//{print $0}' |
	while IFS= read -r line ; do
		echo "RM: ${line:1}"
		rm "${1}${line:1}"
	done
}

CreateBridgeSTPConf() {
	# $1: directory with extracted content
	local tmp="${1}/etc/bridge-stp.conf"
	[ -x "${converterPath}/${1}.awk" ] && return # Will use default or from GWS
	[ -e "${tmp}" ] && return # GWS is providing this file
	grep -Fq -- 'mstpctl addbridge' \
	"${1}/etc/init.d/S"* \
	"${1}/etc/init.d/network"* \
	2>/dev/null \
	&& return # GWS configures mstpctl directly
	
	echo 'MANAGE_MSTPD=n' > "${tmp}"
}

convertConfig() {
	# $1: converter
	# $2: target
	local converter="${converterPath}/${1}.awk"
	local target="${2}"
	[ -x "${converter}" ] || return 0
	[ -s "${target}" ]    || return 0

	local tmp1="$(mktemp -t ${tmpBaseName}-tmp.XXXXXX)"
	cp "${target}" "${tmp1}"
	"${converter}" "${tmp1}" > "${target}"
	gw_rm "${tmp1}"
}

convertConfigFiles() {
	# $1: directory with extracted content

	for f in $(getScriptFileList "${1}"); do
		[ -f "${1}${f}" ] || continue
		case "${f##*/}" in
			S40network|network_eth[1-5])
				convertConfig 'S40-mstpctl' "${1}${f}"
			;;
		esac
	done

	CreateBridgeSTPConf "$1"
}

fixConfigContent() {
	# $1: directory with extracted content
	local CHR="${1}"

	# fix wrong snmpd path (bug of old GWS)
	local tmp="${CHR}/etc/defaults/snmpd"
	[ -f "${tmp}" ] && mv "${tmp}" "${CHR}/etc/default/snmpd"

	# system default specific
	rm "${CHR}/etc/network/interfaces" 2>/dev/null
	rm "${CHR}/md5sums"    2>/dev/null
	rm "${CHR}/setup_info" 2>/dev/null

	deleteUnknownFiles "${CHR}"
}


fixConfigFiles() {
	# $1: directory with extracted content
	local CHR="${1}"

	for f in $(getAllFiles "${1}" | "${filterPath}/GWS-files.awk" -v other=1 sh=1 shconf=1); do
		fixDOS "${CHR}${f}"
	done

	# disable root login
	local tmp="${CHR}/etc/sshd_config"
	[ -f "${tmp}" ] && echo 'PermitRootLogin no' >> "${tmp}"
	local tmp="${CHR}/etc/ssh/sshd_config"
	[ -f "${tmp}" ] && echo 'PermitRootLogin no' >> "${tmp}"

	# remove invalid TZ
	local tmp="${CHR}/etc/TZ"
	testTZ "${tmp}" || echo 'EET-2EEST-3,M3.5.0/03:00:00,M10.5.0/04:00:00' > "${tmp}"

	# remove invalid 'log-conf.xml'
	local tmp="${CHR}/usr/local/etc/telem/log-conf.xml"
	testLogConfXML "${tmp}" || rm "${tmp}"
}

verifyConfigContent() {
	# $1: directory with extracted content
	local CHR="${1}"

# 	if [ -f "${CHR}/GWS/checksums.sha256" ]; then
# 		# TODO signature verification
# 		(
# 			cd "${CHR}/checksums.sha256"
# 			sha256sum -c md5sum.txt || return 1
# 		)
# 	elif [ -f "${CHR}/GWS/md5sum.txt" ]; then
# 		(
# 			cd "${CHR}/GWS"
# 			md5sum -c md5sum.txt || return 1
# 		)
# 	fi
	return 0
}

##############################################
testAndExtractTarxz() {
	# $1: archive path
	# $2: directory for extracted content
	testConfigArchive "${1}"    || return 1
	extractConfig "${1}" "${2}" || return 1
}

testAndFixContent() {
	# $1: directory with extracted content
	convertConfigFiles "${1}"
	fixConfigContent "${1}"
	testFileList     "${1}" || return 1
	fixConfigFiles   "${1}"
	testScriptFiles  "${1}" || return 1
	testOtherFiles   "${1}" || return 1
}

fixContent() {
	# $1: directory with extracted content
	convertConfigFiles "${1}"
	fixConfigContent "${1}"
	fixConfigFiles   "${1}"
}

needToTest () {
	# $1: archive path
	$systemDefaultIsCorrect && isSystemDefault "${1}" && return 1
	$gw_doTest
}

##############################################
extractAndTest() {
	# $1: archive path
	# $2: directory for extracted content
	testAndExtractTarxz "${1}" "${2}" || return 1
	if needToTest "${1}"; then
		testAndFixContent "${2}" || return 1
	else
		gw_log_verbose "Skip testing"
		fixContent "${2}"
	fi
	true
}

extractAndTestTarXz() {
	# $1: archive path
	local TMP_SETUP="$(mktemp -d -t ${tmpBaseName}-setup.XXXXXX)"
	extractAndTest "${1}" "${TMP_SETUP}" \
	|| { rm -r "${TMP_SETUP}"; return 1; }
	rm -r "${TMP_SETUP}"
	return 0
}
##############################################
