#!/bin/dash
# Simple Bluetooth Manager
#
# Copyright (C) James Budiono 2021
# License: GPL Version 3 or later
#

### configuration
APPTITLE="Simple Bluetooth Manager"
DOWNLOAD_FOLDER=$HOME/Downloads
BTCONF=/etc/bluetooth/main.conf
DEVCLASS_URL="http://bluetooth-pentest.narod.ru/software/bluetooth_class_of_device-service_generator.html"
AVAIL_DISCOVERY_DURATIONS="
	30 \"30 seconds\"
	15 \"15 seconds\"
	10 \"10 seconds\"
	 5 \"5 seconds\"	
"
AVAIL_BROADCAST_DURATIONS="
	 60 \"1 minute\"
	180 \"3 minutes\"
	300 \"5 minutes\"
	100 \"10 minutes\"	
"



########## UI helpers #########

# $1-info
msgbox() {
	if [ $DISPLAY ]; then
		Xdialog --title "$APPTITLE" --msgbox "$1" 0 0 10000
	else
		dialog --backtitle "$APPTITLE" --msgbox "$1" 0 0 > /dev/stderr
	fi
}

# $1-info $2-timeout (in ms)
infobox() {
	local timeout=${2:-10000}
	if [ $DISPLAY ]; then
		Xdialog --title "$APPTITLE" --infobox "$1" 0 0 $timeout
	else
		dialog --backtitle "$APPTITLE" --no-cancel --pause "$1" 12 60 $(( timeout/1000 )) > /dev/stderr
	fi
}

# $1-text, $2-choices, output: stdout
choices() {
	if [ $DISPLAY ]; then
		eval Xdialog --title \"$APPTITLE\" --stdout --no-tags --menubox \""$1"\" 20 100 5 $2
	else 
		eval dialog --backtitle \"$APPTITLE\" --stdout --no-tags --menu \""$1"\" 0 0 0 $2
	fi
}

# $1-text, $2-choices, output: stdout
multi_choices() {
	if [ $DISPLAY ]; then
		eval Xdialog --title \"$APPTITLE\" --stdout --no-tags --separator \" \" --checklist \""$1"\" 20 100 5 $2
	else
		eval dialog --title \"$APPTITLE\" --stdout --no-tags --separator \" \" --checklist \""$1"\" 0 0 0 $2
	fi
}

# $1-text
yesno() {
	if [ $DISPLAY ]; then
		Xdialog --title "$APPTITLE" --yesno "$1" 0 0
	else
		dialog --title "$APPTITLE" --yesno "$1" 0 0
	fi
}

# $1-prompt, $2-current value
inputbox() {
	if [ $DISPLAY ]; then
		Xdialog --title "$APPTITLE" --stdout --inputbox "$1" 0 0 "$2"
	else
		dialog --title "$APPTITLE" --stdout --inputbox "$1" 0 0 "$2"		
	fi
}

# $1-prompt
fselect() {
	if [ $DISPLAY ]; then
		Xdialog --title "$APPTITLE" --backtitle "$1" --stdout --no-buttons --fselect "$PWD" 0 0
	else
		dialog --title "$APPTITLE" --backtitle "$1" --stdout --fselect "$PWD"/ 10 70
	fi
}

# $1-prompt
tailbox() {
	local tmpfile=/tmp/bt-status.$$
	if [ $DISPLAY ]; then
		Xdialog --title "$APPTITLE" --no-cancel --backtitle "$1" --tailbox - 0 0
	else
		cat > $tmpfile
		dialog --title "$APPTITLE" --backtitle "$1" --textbox $tmpfile 0 0
		rm $tmpfile
	fi
}

# $1-text
splash() {
	if [ "$XPID" ]; then
		kill $XPID
		XPID=
	fi

	if [ "$1" ]; then	
		if [ $DISPLAY ]; then
			Xdialog --title "$APPTITLE" --no-buttons --infobox "$1" 0 0 1000000 &
			XPID=$!
		else
			dialog --backtitle "$APPTITLE" --infobox "$1" 0 0 > /dev/stderr
		fi
	fi
}

# $1-text
die() {
	infobox "$1"
	exit
}

# $1-option, $* options
has_option() {
	local p inp=$1
	shift
	for p; do
		case $p in
			$inp) return 0
		esac	
	done
	return 1
}




########## Bluetooth helpers ############

get_adapters() {
	hciconfig | awk '
$1 ~ /^hci/ { sub(/:/,"",$1); idx=$1 }
$0 ~ /BD Address/ {  adapters[idx] = $3 }
END {
	for (i in adapters) {
		print i " " adapters[i]	
	}
}'
}

get_paired_devices() {
	bt-device -l | awk '
$1 !~ /^Added/ {
	address=$NF; sub(/\(/,"",address); sub(/\)/,"",address); printf address " "
	name=""
	for (i=1; i<NF; i++) name=sprintf("%s %s",name,$i)
	sub(/ /,"",name); print "\"" name "\""
}'
}

parse_discovery_output() {
	awk '
/^\[/ { addr=$1; sub(/\[/,"",addr); sub(/\]/,"",addr); devices[addr]=0; }
/Name: / { name=$0; sub(/.*Name: /,"",name); if (name!="(null)") devices[addr]=name; }
/Alias: / { alias=$0; sub(/.*Alias: /,"",alias);
	if (alias != "(null)" && !devices[addr]) devices[addr]=alias;
}
END {
	for (idx in devices) {
		print idx " \"" devices[idx] "\""
	}
}'
}



########## Choosers UI ############

# out: BT_DEV
select_adapter() {
	local adapters=
	set -- $(get_adapters)
	while [ $1 ]; do
		adapters="$adapters $1 \"$1 ($2)\""
		shift 2
	done
	[ -z "$adapters" ] && return 1
	BT_DEV=$(choices "Choose adapters" "$adapters")
}

# $1-label, $2-choices (mac devname pairs) out: BT_DEV, BT_DEVNAME
select_devices() {
	local devices= label="$1" ret=
	eval set -- $2
	while [ "$1" ]; do
		devices="$devices $1 \"$1 - $2\""
		shift 2
	done
	BT_DEV=$(choices "$label" "$devices")
	ret=$?
	if [ "$BT_DEV" ]; then
		BT_DEVNAME=$(get_paired_devices | sed -n "/$BT_DEV/ {s/.* \"/\"/;p}")
	fi
	return $ret
}



########### Tasks ############

# $1-up/down
adapter_updown() {
	select_adapter || return 1
	if [ -z "$BT_DEV" ]; then
		infobox "No bluetooth adapter detected."
		return 1
	fi
	if [ "$1" = "up" ] && service bluetooth status | grep -q stopped; then
		infobox "Cannot bring up adapter if bluetooth service is stopped."
		return
	fi
	if hciconfig $BT_DEV $1; then
		infobox "$BT_DEV is brought ${1}."
	else
		infobox "Unable to bring $BT_DEV ${1}."
	fi
}

list_paired_devices() {
	select_devices "List of all paired devices" "$(get_paired_devices)"
}

#$1-connect/disconnect/unpair
bt_device_action() {
	select_devices "Choose the device you want to $1" "$(get_paired_devices)" || return 1
	if [ -z "$BT_DEV" ]; then
		infobox "No paired bluetooth devices found."
		return 1
	fi
	if case $1 in
		connect)
			splash "Attempting to connect to $BT_DEVNAME ..."
			bt-device -c $BT_DEV
		;;
		disconnect)
			bt-device -d $BT_DEV
		;;
		unpair)
			if yesno "Are you sure you want to unpair $BT_DEVNAME ($BT_DEV) ?"; then
				bt-device -r $BT_DEV
			else
				return 
			fi
		;;
	esac; then
		splash ""
		infobox "$BT_DEVNAME ($BT_DEV) now ${1}ed."
	else
		splash ""
		infobox "$BT_DEVNAME ($BT_DEV) failed to ${1}."
	fi
}

discover_devices() {
	local timeout= pid tmpfile=/tmp/bt-discover.$$

	timeout=$(choices "Choose discovery duration" "$AVAIL_DISCOVERY_DURATIONS") || return 1
	msgbox "Set your devices in discovery mode. Press OK when ready ..."

	bt-adapter -d > $tmpfile &
	pid=$!
	infobox "Finding new devices for $timeout seconds. Cancel anytime by clicking OK." $(( timeout*1000 ))
	kill $pid
	parse_discovery_output < $tmpfile
	rm $tmpfile
}

pair_device() {
	local devices= pin= ret=
	yesno "Pairing by default works for devices that don't require or
only require simple yes/no authorisation.

If it fails, you'll be prompted to try again using PIN authorisation.
In case if it fails, too, you need to use other tools such as
bluetoothctl.

Continue?
" || return 1 

	# discovery process
	devices=$(discover_devices) || return 1	
	if [ -z "$devices" ]; then
		infobox "Cannot find any devices. Please try again."
		return 1
	fi

	select_devices "Choose the device you want to pair" "$devices"
	ret=$?

	# remove all temporarily paired devices, except the chosen one (if any)
	echo "$devices" | while IFS= read dev; do
		#echo ">>> Removing: $dev"	# debug
		[ "${dev%% *}" = "$BT_DEV" ] && continue
		bt-device -r ${dev%% *}
	done
	
	[ $ret -ne 0 ] && return 1
	if [ -z "$BT_DEV" ]; then
		infobox "No paired bluetooth devices found."
		return 1
	fi

	# pairing process
	splash "Attempting to pair with $BT_DEVNAME ($BT_DEV) using no/simple authorisation..."
	if ! yes yes | bt-device -p $BT_DEV; then
		splash ""
		if ! yesno "Failed to pair with $BT_DEVNAME ($BT_DEV).\nTry again using PIN authorisation?"; then
			bt-device -r $BT_DEV
			return 1
		fi

		sleep 2	# K750i needs a small delay before another try
		pin=$(tr -dc 0-9 < /dev/urandom | head -c4)
		splash "Attempting to pair with $BT_DEVNAME ($BT_DEV) using PIN authorisation...
When prompted, type this PIN in your device: $pin"

		if ! echo "$pin" | bt-device -p $BT_DEV; then
			bt-device -r $BT_DEV
			splash ""
			infobox "Failed to pair with $BT_DEVNAME ($BT_DEV)."	
			return 1
		fi
	fi

	splash ""
	infobox "Successfully paired with $BT_DEVNAME ($BT_DEV)."
}

make_computer_discoverable() {
	local timeout= pid=
	yesno "Turning on discovery mode makes your computer discoverable,
so that other devices can find and pair with it. This is usually not
necessary, you usually make the __other__ devices discoverable instead,
so that this computer can find them.

You only to turn on discovery mode if the other devices cannot make
themselves discoverable, or if you want to pair two computers, then
one of them have to make itself discoverable.

Enabling this also enables your computer to automatically accept
pairing requests from others devices. Automatic pairing works for
simple devices that don't require or only require simple yes/no
authorisation. Devices that require you to use more complex
authorisation are __NOT__ supported.
In this case, you need to use other tools such as bluetoothctl.

Continue?
" || return 1
	timeout=$(choices "Choose how long you want to make the computer discoverable.
You can always terminate earlier by closing the dialog box.
" "$AVAIL_BROADCAST_DURATIONS") || return 1

	bt-adapter -s DiscoverableTimeout $timeout
	bt-adapter -s Discoverable on
	yes yes | bt-agent &
	pid=$!

	infobox "Computer is now discoverable and pairable for $timeout seconds.
Click OK to cancel discovery mode at anytime.
" $(( timeout*1000 ))
	kill $pid
	bt-adapter -s Discoverable off
}

# $1-the idiot, $2=result
check_if_supported() {
	if [ -z $DISPLAY ]; then
		msgbox "Due to some idiotic decisions from bluez and/or dbus,
$1 will refuse to run without an X11 server.
Thus $2 will not work from terminal.

Sorry."
		return 1
	fi
}

receive_files() {
	local pid=

	check_if_supported "obexd" "sending and receiving files" || return 1
	
	[ -e "$DOWNLOAD_FOLDER" ] || mkdir -p "$DOWNLOAD_FOLDER"
	obexd -l -a -r "$DOWNLOAD_FOLDER" &
	pid=$!
	
	msgbox "Receiver is now running. Files are stored in $DOWNLOAD_FOLDER.
Press okay to close when done."

	kill $pid
}

send_files() {
	local pid= fn= result=

	check_if_supported "obexd" "sending and receiving files" || return 1
	
	select_devices "Choose the device you want to $1" "$(get_paired_devices)" || return 1
	if [ -z "$BT_DEV" ]; then
		infobox "No paired bluetooth devices found."
		return 1
	fi

	obexd -l -a -r "$DOWNLOAD_FOLDER" &
	pid=$!

	while : true; do
		fn=$(fselect "Choose the file you want to send") || break
		[ -z "$fn" ] && break
		splash "Sending $fn, please wait ..."
		if bt-obex -p $BT_DEV "$fn"; then
			result="completed"
		else
			result="failed."
		fi
		splash ""
		yesno "Sending $fn $result.
Do you want to send another file?
" || break
	done
set +x
	kill $pid
}

rename_computer() {
	local curname= newname=
	if [ $(id -u) -ne 0 ]; then
		infobox "You must be root do to this."
		return
	fi

	curname=$(sed -n '/Name[ \t]*=/ {s/.*=[ \t]*//;s/[ \t]*$//;p}' $BTCONF)
	newname=$(inputbox "Choose new computer name" "$curname") || return 1
	if [ -z "$newname" ]; then
		infobox "Name cannot be blank."
		return
	fi
	if [ "$newname" = "$curname" ]; then
		infobox "Old and new name is identical, nothing is changed."
		return
	fi
	sed -i -e "/Name[ \t]*=/ s|.*|Name = $newname|" $BTCONF

	splash "Computer name changed to $curname, restarting bluetooth service ..."
	service bluetooth restart
	splash ""
}

set_device_class() {
	local curclass= newclass=
	if [ $(id -u) -ne 0 ]; then
		infobox "You must be root do to this."
		return
	fi

	yesno "Do you need help to determine the device class?
If yes, click 'YES' and the following website will be opened for you in your browser.

$DEVCLASS_URL

The website helps you to conjure the correct device class based on the features you want.

"   && defaultbrowser "$DEVCLASS_URL"

	curclass=$(sed -n '/Class[ \t]*=/ {s/.*=[ \t]*//;s/[ \t]*$//;p}' $BTCONF)
	newclass=$(inputbox "Choose new host device class.
Enter value in hexadecimal. Include the 0x prefix.

0x000100 -> Computer (default)
0x100100 -> Computer and Object Transfer (most likely what you want)
0xBE0100 -> Computer and kitchen sink

" "$curclass") || return 1
	if [ -z "$curclass" ]; then
		infobox "Device class cannot be blank."
		return
	fi
	if [ "$newclass" = "$curclass" ]; then
		infobox "Old and new class is identical, nothing is changed."
		return
	fi
	sed -i -e "/Class[ \t]*=/ s|.*|Class = $newclass|" $BTCONF

	splash "Device class changed to $newclass, restarting bluetooth service ..."
	service bluetooth restart
	splash ""
}

show_host_status() {
	{
		local curname=$(sed -n '/Name[ \t]*=/ {s/.*=[ \t]*//;s/[ \t]*$//;p}' $BTCONF)
		local curclass=$(sed -n '/Class[ \t]*=/ {s/.*=[ \t]*//;s/[ \t]*$//;p}' $BTCONF)
		
		printf "Bluetooth hostname: $curname\n"
		printf "Device class: $curclass\n"
		printf "Service: $(service bluetooth status)\n\n"
		printf "Host adapters\n------------\n"
		hciconfig
	} | tailbox
}

# $1-start/stop
svc() {
	if [ $(id -u) -ne 0 ]; then
		infobox "You must be root do to this."
		return
	fi
	splash "${1}ing service, please wait ..."
	service bluetooth $1
	splash ""
	infobox "${1}ing service completed."
}

########### main ############

AVAIL_TASKS="
connect    \"1. Connect to a paired device\"
disconnect \"2. Disconnect device\"
___        \"____________________\"
receive    \"3. Receive files from device\"
send       \"4. Send files to device\"
___        \"____________________\"
list       \"5. List paired devices\"
pair       \"6. Find and pair a new device\"
unpair     \"7. Unpair an existing device\"
broadcast  \"8. Turn on discovery mode\"
___        \"____________________\"
status     \"9. Host status\"
rename     \"A. Rename computer\"
class      \"B. Set computer device class\"
startsvc   \"C. Start/re-start bluetooth service\"
stopsvc    \"D. Stop bluetooth service\"
up         \"E. Bring host adapter up\"
down       \"F. Bring host adapter down\"
quit       \"Quit\"
"

while [ "$TASK" != quit ]; do
	TASK=$(choices "Highlight what you want to do, then press Enter" "$AVAIL_TASKS") || TASK=quit
	case $TASK in
		up)         adapter_updown up ;;
		down)       adapter_updown down ;;
		list)       list_paired_devices ;;
		connect)    bt_device_action connect ;;
		disconnect) bt_device_action disconnect ;;
		unpair)     bt_device_action unpair ;;
		pair)       pair_device ;;
		broadcast)  make_computer_discoverable ;;
		receive)    receive_files ;;
		send)       send_files ;;
		rename)     rename_computer ;;
		class)      set_device_class ;;
		status)     show_host_status ;;
		startsvc)   svc start ;;
		stopsvc)    svc stop ;;		
	esac
done
infobox "Done."

