#!/usr/bin/env bash

# Uncomment to get core dumps if segfaults are happening.
ulimit -c unlimited

myscript=$(basename $0)
if [ ! -f "$myscript" ] ; then
	echo "Please run $myscript whilst standing in the same directory" 1>&2
	exit 1
fi

target="$1"
burpbin="$2"
clientconf="$3"
clientconfarg="$4"
includedir="$5"
restoredir="$6"
serveraddress="$7"
sshclient="$8"
scpclient="$9"
clientaddress="${10}"

if [ -z "$target" ] ; then
	echo "target directory not given."
	exit 1
fi
if [ -z "$burpbin" ] ; then
	echo "location of burp binary not given."
	exit 1
fi
if [ -z "$clientconf" ] ; then
	echo "location of client config file not given."
	exit 1
fi
if [ -z "$clientconfarg" ] ; then
	echo "client config arg not given."
	exit 1
fi
if [ -z "$includedir" ] ; then
	echo "location of directory to backup not given ."
	exit 1
fi
if [ -z "$restoredir" ] ; then
	echo "location of directory to restore to not given ."
	exit 1
fi
if [ -z "$serveraddress" ] ; then
	echo "server address not given ."
	exit 1
fi

# The Windows build symlinks cock things for 'diff -ur' on the restore later,
# so delete them first.
rm -f ../burp-depkgs
rm -f ../burp-cross-tools

excludedir="$includedir/src"
logs="$PWD/logs"
serverlog="$logs/server.log"
clientlog="$logs/client.log"
difflog="$logs/diff.log"
serverpid=
serverconf=etc/burp/burp-server.conf
serverscript="$PWD/serverscript"
clientscript="$PWD/clientscript"
num=0 # Test number. Incremented for each test.
bno=0 # Backup number. Incremented for each backup.
pre=0 # Previous backup number.
res=0 # Restore number. Incremented for each restore.
test_includes_backup=0
test_includes_restore=0
protocol=2

if stat --version 2>/dev/null | grep GNU &>/dev/null ; then
	# GNU style
	stat_args="-c '%g:%u %A %N'"
else
	# FreeBSD style
	stat_args="-f '%g:%u %p %N %Y'"
fi

kill_server()
{
	if [ -n "$serverpid" ] ; then
		echo "Killing test server"
		kill -9 $serverpid
		serverpid=
	fi
}

trap "kill_server" 0 1 2 3 15

fail()
{
	backup_and_restore_nums=$(get_backup_and_restore_nums)
	echo
	echo "Test $num failed: $@ $backup_and_restore_nums"
	echo
	kill_server
	exit 1
}

makedir_server()
{
	rm -rf "$1"
	mkdir -p "$1" || fail "could not mkdir $1"
}

makedir_client()
{
	cat >> "$clientscript" << EOF
rm -rf "$1"
mkdir -p "$1" || fail "could not mkdir $1"
EOF
}

cdir()
{
	cd "$1" || fail "could not cd to $1"
}

sed_rep_server()
{
	conf="$serverconf"
	[ -n "$2" ] && conf="$2"
	cat >> "$serverscript" << EOF
sed -i -e "$1" "$conf" || fail "sed $1 failed $2"
EOF
}

append_server()
{
	conf="$serverconf"
	[ -n "$2" ] && conf="$2"
	cat >> "$serverscript" << EOF
echo "$1" >> "$conf" || fail "echo $1 failed"
EOF

}
sed_rep_client()
{
	cat >> "$clientscript" << EOF
sed -i -e "$1" "$clientconf" || fail "sed $1 failed $2"
EOF
}

append_client()
{
	conf="$clientconf"
	[ -n "$2" ] && conf="$2"
	cat >> "$clientscript" << EOF
echo "$1" >> "$conf" || fail "echo $1 failed"
EOF
}

wait_for_backup_to_finish()
{
	local waited=0
	local lockfile="$target/var/spool/burp/testclient/lockfile"
	[ "$protocol" = "2" ] && \
	  lockfile="$target/var/spool/burp/global/clients/testclient/lockfile"

	SLEEPTIME=2
	# Repeatedly check to see whether the server has finished, rather
	# than just sleeping some length of time. 
	while true ; do
		sleep $SLEEPTIME
		[ ! -e "$lockfile" ] && break
		read pid < "$lockfile"
		[ -z "$pid" -o ! -d "/proc/$pid" ] && break
		waited=$((waited+$SLEEPTIME))
		[ "$waited" -gt 120 ] && \
		  fail "client backup seemed to be complete after 2 minutes"
	done
}

add_run_list()
{
cat >> "$clientscript" <<EOF
echo "Starting test client list"
"$burpbin" -c "$clientconfarg" -a l -b "$1" || fail "client list returned \$?"

EOF
}

add_run_list_expect_fail()
{
cat >> "$clientscript" <<EOF
echo "Starting test client list"
"$burpbin" -c "$clientconfarg" -a l -b "$1" && fail "client list returned \$?"

EOF
}

add_run_backup_no_increment()
{
cat >> "$clientscript" <<EOF
echo "Starting test client backup"
"$burpbin" -c "$clientconfarg" -a b || fail "client backup returned \$?"

EOF
}

increment()
{
	pre=$bno
	bno=$((bno+1))
}

add_run_backup()
{
	increment
	test_includes_backup=1
	add_run_backup_no_increment
}

add_run_backup_expect_fail_no_increment()
{
cat >> "$clientscript" <<EOF
echo "Starting test client backup"
"$burpbin" -c "$clientconfarg" -a b && fail "client backup returned \$? (expected non-zero)"

EOF
}

add_run_backup_expect_fail()
{
	increment
	add_run_backup_expect_fail_no_increment
}

add_run_verify()
{
	local bno="all"
cat >> "$clientscript" <<EOF
echo "Starting test client verify of backup $bno"
"$burpbin" -c "$clientconfarg" -a v -b "$bno" \
		|| fail "client verify returned \$?"
EOF
}

add_run_restore()
{
	local bno="$1"
	local dir
	res=$((res+1))
	dir="$2$res"
	test_includes_restore=1
	makedir_client "$dir"
# If vss has been stripped, need to add the '-X' option to the restore.
	nowinapi=
	[ -n "$STRIP_VSS" -a "$protocol" = 1 ] && nowinapi='-X'
cat >> "$clientscript" << EOF
echo "Starting test client restore of backup $bno"
rm -rf "$dir" || fail

"$burpbin" -c "$clientconfarg" -a r -b "$bno" -d "$dir" $nowinapi \
	|| fail "client restore returned \$?"
EOF
}

add_run_restore_server_initiated()
{
	local bno="$1"
	local dir
	res=$((res+1))
	dir="$2$res"
	test_includes_restore=1
	local srestore="$target/var/spool/burp/testclient/restore"

	append_client 'restoreprefix = /'

	[ "$PROTOCOL" = "2" ] && \
	  srestore="$target/var/spool/burp/global/clients/testclient/restore"

	echo "backup=$bno
restoreprefix=$dir" > "$srestore"
	makedir_client "$dir"
cat >> "$clientscript" << EOF
echo "Starting test server initiated restore of backup $bno"
rm -rf "$dir" || fail

"$burpbin" -c "$clientconfarg" -a l \
	|| fail "server initiated restore returned \$?"
EOF
}

add_run_snapshot()
{
	cat >> "$clientscript" <<EOF
[ -d "/cygdrive" ] \
	&& echo "Status snapshot not supported on Windows" && exit 0
"$burpbin" -c "$clientconfarg" -a S | grep testclient \
	|| fail "Status snapshot did not show testclient"
"$burpbin" -c "$clientconfarg" -a S -C testclient | grep 0000001 \
	|| fail "Status snapshot did not show backup 0000001"
EOF
}

do_diff_server()
{
	diff -ur "$1" "$2" >>"$difflog" 2>&1 || fail "$3"
}

add_do_diff_client()
{
	# On Windows, /C:/ gets converted to /C_/, so tweak the path for diff.
	rpath1=$(echo "$1" | sed -e 's/\/C:\//\/C_\//g')
	rpath2=$(echo "$2" | sed -e 's/\/C:\//\/C_\//g')
	# Protocol 2 directory permissions are messed up on windows, so
	# exclude them for now.
	extra_find_arg=
	if [ "$protocol" = "2" ] \
	  && [ "$restoredir" = "C:/Program Files/Burp/restore" ] ; then
		extra_find_arg="-type f"
	fi

	# FIX THIS: On windows, the builders directory is being EFS
	# encrypted. But, for some reason, the permissions come out
	# wrong on restore. Exclude them from the permission check
	# for now.
	exclude_dir=
	if [ "$restoredir" = "C:/Program Files/Burp/restore" ] ; then
		exclude_dir="| grep -v builders"
	fi

cat >> "$clientscript" << EOF
diff -ur "$rpath1" "$rpath2" || fail "$3"
EOF

	# Stripping vss in protocol 1 will of course cause permissions to be
	# wrong on restore on Windows, so do not do this check in that case.
[ -z "$STRIP_VSS" -o "$protocol" = 2 ] && cat >> "$clientscript" << EOF
diff -u \
  <( cd "$rpath1" ; find . $extra_find_arg -print0 | xargs -0 stat $stat_args | sort $exclude_dir) \
  <( cd "$rpath2" ; find . $extra_find_arg -print0 | xargs -0 stat $stat_args | sort $exclude_dir) \
	|| fail "$3 (permissions)"

	rm -rf "$rpath2"
EOF
}

add_chown()
{
	cat >> "$clientscript" << EOF
chmod "$1" "$2" || fail "chown $1 $2 failed"
EOF
}

write_message()
{
	message="$1"
	echo "$message"
	echo "$message" >> $serverlog
	echo "$message" >> $clientlog
	echo "$message" >> $difflog
}

start_test()
{
	msg="$1"
	num=$((num+1))
	test_includes_backup=0
	test_includes_restore=0
	write_message "
Test $num
$msg"
	add_normal_settings
}

add_change_source_files()
{
cat >> "$clientscript" <<EOF
cp -r "$includedir/src" "$includedir/src-new" \
	|| fail "could not cp $includedir/src to $includedir/src-new"

# Move a directory, which simulates deleting and adding new files.
if [ -d "$includedir/configs" ] \
 && [ ! -e "$includedir/configs-new" ] ; then
	mv "$includedir/configs" "$includedir/configs-new" \
	    || fail "could not move $includedir/configs to $includedir/configs-new"
elif [ ! -e "$includedir/configs" ] \
 && [ -d "$includedir/configs-new" ] ; then
   mv "$includedir/configs-new" "$includedir/configs" \
    || fail "could not move $includedir/configs-new to $includedir/configs"
else
	fail "could not move directory to simulate deletion/addition"
fi

# Scramble a whole bunch of files
# On Windows, need to specify the whole path to find and sort, or the Windows
# versions are picked up instead.
sortprog="sort --random-sort"
which shuffle >/dev/null 2>&1 && sortprog=shuffle
/usr/bin/find "$includedir/autoconf" -type f | while read f ; do
	LC_ALL='C' \$sortprog "\$f" > tmpfile
	[ "$?" != "0" ] && exit 1
	mv tmpfile "\$f" || exit 1
done || fail "randomise files failed"
EOF
}

add_compression_off()
{
	sed_rep_server '/^compression = .*/d'
	append_server 'compression = 0'
}

add_compression_on()
{
	sed_rep_server '/^compression = .*/d'
	append_server 'compression = 9'
}

add_encryption_off()
{
	sed_rep_client '/^encryption_password = .*/d'
}

add_encryption_on()
{
	add_encryption_off
	append_client 'encryption_password = 012345678901234567890123456789'
}

add_include_off()
{
	sed_rep_client '/^include = .*/d'
}

add_include_on()
{
	add_include_off
	append_client "include = $includedir"
}

add_split_vss_off()
{
	sed_rep_client '/^split_vss = .*/d' "$clientconf"
}

add_split_vss_on()
{
	add_split_vss_off
	append_client "split_vss = 1"
}

add_strip_vss_off()
{
	sed_rep_client '/^strip_vss = .*/d' "$clientconf"
}

add_strip_vss_on()
{
	add_strip_vss_off
	append_client "strip_vss = 1"
}

add_protocol_off()
{
	sed_rep_client '/^protocol = .*/d' "$clientconf"
}

maybe_add_protocol2_on()
{
	add_protocol_off
	[ "$protocol" = 2 ] && \
		append_client 'protocol = 2'
}

add_server_listen_status_off()
{
	sed_rep_server '/^listen_status = .*/d' "$serverconf"
}

add_server_listen_status_on()
{
	add_server_listen_status_off
	append_server "listen_status = 127.0.0.1:4999"
}

add_server_breakpoint_off()
{
	sed_rep_server '/^breakpoint = .*/d' "$serverconf"
}

add_server_breakpoint_on()
{
	add_server_breakpoint_off
	append_server "breakpoint = $1"
}

add_client_breakpoint_off()
{
	sed_rep_client '/^breakpoint = .*/d' "$clientconf"
}

add_client_breakpoint_on()
{
	add_client_breakpoint_off
	append_client "breakpoint = $1"
}

add_client_server_can_restore_off()
{
	sed_rep_client '/^server_can_restore = .*/d' "$clientconf"
}

add_client_server_can_restore_on()
{
	add_client_server_can_restore_off
	append_client "server_can_restore = 1"
}

add_recovery_method_delete()
{
	sed_rep_server 's/^working_dir_recovery_method = .*/working_dir_recovery_method = delete/g'
}

add_recovery_method_resume()
{
	sed_rep_server 's/^working_dir_recovery_method = .*/working_dir_recovery_method = resume/g'
}

makedir_server "$logs"

cdir "$target"

start_script()
{
	cat > "$1" << EOF
#!/usr/bin/env bash

fail()
{
	echo
	echo "Test $num failed: \$@"
	echo
	exit 1
}

EOF
}

start_script_server()
{
	start_script "$serverscript"
}

start_script_client()
{
	start_script "$clientscript"
}

# Tweak the example configuration files by removing the leading '/' from all
# the paths, and changing the port numbers.
pre_tweaks_server()
{
	start_script_server
	sed_rep_server 's#= /#= '"$target"'/#g'
	sed_rep_server 's/listen = 0.0.0.0:4971/listen = 0.0.0.0:4998/g'
	sed_rep_server 's#^CA_DIR.*#CA_DIR = '"$target"'/etc/burp/CA#g' "$target/etc/burp/CA.cnf"
	sed_rep_server 's/^stdout = .*/stdout = 1/g'
	sed_rep_server 's/^syslog = .*/syslog = 0/g'
}

pre_tweaks_client()
{
	start_script_client
	sed_rep_client 's#= /#= '"$target"'/#g'
	sed_rep_client 's/port = 4972/port = 4999/g'
	sed_rep_client 's/^stdout = .*/stdout = 1/g'
	sed_rep_client 's/^syslog = .*/syslog = 0/g'
	sed_rep_client 's/^server = .*/server = '"$serveraddress":4998'/g' "$clientconf"
	sed_rep_client 's/^cname = .*/cname = testclient/g' "$clientconf"
	sed_rep_client 's/^password = .*/password = abcdefgh/g' "$clientconf"
}

run_script_server()
{
	echo 'exit 0' >> "$serverscript"
	chmod 755 "$serverscript" \
		|| fail "chmod 755 $serverscript failed"
	"$serverscript" >> "$serverlog" \
		|| fail "$serverscript failed"
	rm -f "$serverscript"
}

run_script_client()
{
	echo 'exit 0' >> "$clientscript"
	if [ -n "$sshclient" ] ; then
		bname=$(basename "$clientscript")
		$scpclient "$clientscript" "$clientaddress:" \
			|| fail "Could not copy $clientscript to client"
		$sshclient $clientaddress chmod 755 "$bname" \
			|| fail "Could not chmod 755 $clientscript on client"
		$sshclient $clientaddress "./$bname" >> "$clientlog" \
			|| fail "$clientscript failed on client"
		$sshclient $clientaddress rm -f "$clientscript"
	else
		chmod 755 "$clientscript" \
			|| fail "chmod 755 $clientscript failed"
		"$clientscript" >> "$clientlog" \
			|| fail "$clientscript failed"
	fi
	rm -f "$clientscript"
}

get_backup_and_restore_nums()
{
	if [ $test_includes_backup != 0 ] ; then
		if [ $test_includes_restore != 0 ] ; then
			echo "(backup $bno, restore $res)"
		else
			echo "(backup $bno)"
		fi
	else
		if [ $test_includes_restore != 0 ] ; then
			echo "(restore $res)"
		fi
	fi
}

end_test()
{
	run_script_client
	backup_and_restore_nums=$(get_backup_and_restore_nums)
	write_message "Test $num OK $backup_and_restore_nums"
	sleep 3
}

run_scripts()
{
	run_script_server
	run_script_client
}

add_backup_run_scripts_setup_verify_restore()
{
	add_run_backup
	run_scripts
	wait_for_backup_to_finish
	start_script_client
	add_run_verify
	add_run_restore $bno "$restoredir"
}

add_normal_settings()
{
	start_script_server
	start_script_client
	maybe_add_protocol2_on
	add_compression_on
	add_encryption_off
	add_include_on
	add_server_breakpoint_off
	add_client_breakpoint_off
	add_recovery_method_resume
	add_server_listen_status_on

	# Windows options
	if [ -n "$SPLIT_VSS" ] ; then
		add_split_vss_on
	else
		add_split_vss_off
	fi
	if [ -n "$STRIP_VSS" ] ; then
		add_strip_vss_on
	else
		add_strip_vss_off
	fi
}

add_restore_diff()
{
	add_do_diff_client \
		"$includedir" \
		"$restoredir"$res/"$includedir" \
		"client restore differed from the original!"
}

if [ -z "$NO_CA_GEN" ] ; then
	pre_tweaks_server
	pre_tweaks_client
	run_scripts
fi

echo
echo "Starting tests"
echo "       Server log: $serverlog"
echo "       Client log: $clientlog"
echo "         Diff log: $difflog"
echo "More logs can be found in:"
echo "$target/var/spool/burp/testclient/<backup number>"
echo

if [ -z "$NO_CA_GEN" ] ; then
	echo "Initialising server certificate authority"
	# Need to do this outside of the server,
	# because it might take a long time.
	./usr/sbin/burp -c "$serverconf" -g >> "$serverlog" 2>&1 \
		|| fail "Initialising server certificate authority failed"
	echo
fi

restart_server()
{
	kill_server
	# Start up the server
	echo "Starting test server"
	./usr/sbin/burp -c "$serverconf" -F >> "$serverlog" 2>&1 &
	serverpid=$!

	# Be kind, and wait a little for it to start.
	sleep 5
}

sighup_server()
{
	kill -1 $serverpid
}

comp_enc_change_test()
{
	local comp="$1"
	local encr="$2"
	local change="$3"

	start_test "Compression $comp, encryption $encr, change files $change"
	add_compression_$comp
	add_encryption_$encr
	[ "$change" = "on" ] && add_change_source_files
	add_chown 644 "$includedir/utest/test_cmd.c"
	add_backup_run_scripts_setup_verify_restore
	add_restore_diff
	end_test
}

check_for_working_dir()
{
	[ -h "$workingdir" ] || fail "$workingdir is not a symlink"
	[ -d "$workingdir/" ] || fail "$workingdir/ is not a directory"
	[ -e "$finishingdir" ] && fail "$finishingdir should not exist"
}

check_for_finishing_dir()
{
	[ -h "$finishingdir" ] || fail "$finishingdir is not a symlink"
	[ -d "$finishingdir/" ] || fail "$finishingdir/ is not a directory"
	[ -e "$workingdir" ] && fail "$workingdir should not exist"
}

all_tests()
{
	start_test "Interrupt server phase1"
	add_server_breakpoint_on 1
	add_run_backup_expect_fail_no_increment
	run_scripts
	check_for_working_dir
	end_test

	start_test "Interrupt client phase1"
	add_client_breakpoint_on 1
	add_run_backup_expect_fail_no_increment
	run_scripts
	check_for_working_dir
	end_test

	comp_enc_change_test on  off off

	# Server should come back after a sighup reload.
	sighup_server

	start_test "Interrupt server phase2"
	add_server_breakpoint_on 2
	add_run_backup_expect_fail_no_increment
	run_scripts
	wait_for_backup_to_finish
	check_for_working_dir
	end_test

	start_test "Interrupt server phase3"
	add_server_breakpoint_on 3
	add_run_backup_no_increment
	run_scripts
	wait_for_backup_to_finish
	check_for_working_dir
	end_test
	
	start_test "Complete phase3"
	add_server_breakpoint_off
	if [ "$protocol" -eq "1" ] ; then
		# Client gives an error on protocol1, while the server does
		# the resume.
		add_run_backup_expect_fail
	else
		add_run_backup
	fi
	test_includes_backup=1
	run_scripts
	wait_for_backup_to_finish
	start_script_client
	add_run_verify
	add_run_restore $bno "$restoredir"
	add_restore_diff
	end_test

	start_test "List"
	add_run_list $bno
	add_run_list_expect_fail 99999
	run_scripts
	end_test

	# Currently no way to pass '-x' via server initiated restore, so skip
	# this test for $STRIP_VSS.
	if [ -z "$STRIP_VSS" ] ; then
		start_test "Server-initiated restore"
		start_script_server
		start_script_client
		add_client_server_can_restore_on
		add_run_restore_server_initiated $bno "$restoredir"
		add_restore_diff
		run_scripts
		end_test
	fi

	start_test "Status monitor snapshot"
	add_run_snapshot
	end_test

	start_test "Interrupt server phase4"
	add_server_breakpoint_on 4
	add_run_backup_no_increment
	run_scripts
	wait_for_backup_to_finish
	check_for_finishing_dir
	end_test

	start_test "Complete phase4"
	add_server_breakpoint_off
	add_run_backup_expect_fail
	test_includes_backup=1
	run_scripts
	wait_for_backup_to_finish
	start_script_client
	add_run_verify
	add_run_restore $bno "$restoredir"
	add_restore_diff
	end_test

	start_test "Interrupt server middle of phase2"
	add_server_breakpoint_on 2010
	add_change_source_files
	add_run_backup_expect_fail_no_increment
	run_scripts
	wait_for_backup_to_finish
	check_for_working_dir
	end_test

	start_test "Interrupt server middle of phase2"
	add_server_breakpoint_on 2100
	add_run_backup_expect_fail_no_increment
	run_scripts
	wait_for_backup_to_finish
	check_for_working_dir
	end_test

	start_test "Interrupt server middle of phase2"
	add_server_breakpoint_on 2200
	add_run_backup_expect_fail_no_increment
	run_scripts
	wait_for_backup_to_finish
	check_for_working_dir
	end_test

	start_test "Complete phase2"
	add_server_breakpoint_off
	add_backup_run_scripts_setup_verify_restore
	add_restore_diff
	end_test

	comp_enc_change_test on  off off
	comp_enc_change_test on  off on
	comp_enc_change_test off off on
	if [ "$PROTOCOL" != "2" ] ; then
		# Encryption not supported in protocol 2.
		comp_enc_change_test off on  on
		comp_enc_change_test on  on  on
	fi

	start_test "Permissions"
	add_chown 755 "$includedir/utest/test_cmd.c"
	add_backup_run_scripts_setup_verify_restore
	add_restore_diff
	end_test
}

run_tests()
{
	protocol="$1"
	num=0
	bno=0
	pre=0
	res=0
	echo
	echo "Running protocol $protocol tests"
	all_tests
}

restart_server

if [ "$PROTOCOL" = 1 ] ; then
	workingdir="$target/var/spool/burp/testclient/working"
	finishingdir="$target/var/spool/burp/testclient/finishing"
elif [ "$PROTOCOL" = 2 ] ; then
	workingdir="$target/var/spool/burp/global/clients/testclient/working"
	finishingdir="$target/var/spool/burp/global/clients/testclient/finishing"
else
	echo "Unknown protocol: $PROTOCOL"
	exit 1
fi

run_tests "$PROTOCOL"

echo
echo "All tests succeeded"
echo

exit 0
