#!/bin/ksh93

typeset -r VERSION='1.0' FPROG=${.sh.file} PROG=${FPROG##*/} SDIR=${.sh.file%/*}
export LC_CTYPE='C.UTF-8'

typeset -A DEFAULTS=(								# coturn defaults
	[DATADIR]="/data/coturn"			# 				/var/lib/turn
	[USR]='webservd'				# run as user. 	turnserver
	[GRP]='webservd'				# run as group.	turnserver
)

LIC='[-?'"${VERSION}"' ]
[-copyright?Copyright (c) 2020 Jens Elkner. Alle Rechte vorbehalten.]
[-license?CDDL 1.0]'

for H in log.kshlib man.kshlib ; do
	X=${SDIR}/$H
	[[ -r $X ]] && . $X && continue
	X=${ whence $H; }
	[[ -z $X ]] && print "$H nicht gefunden - Abbruch." && exit 1
	. $X 
done
unset H

function showUsage {
	typeset WHAT="$1" X='--man'
	[[ -z ${WHAT} ]] && WHAT='MAIN' && X='-?'
    getopts -a "${PROG}" "${ print ${Man.FUNC[${WHAT}]}; }" OPT $X
}

function installCoturn {
	Log.info $0
	[[ ${ uname -s; } == 'Linux' ]] || \
	{ Log.fatal 'Derzeit wird nur Ubuntu supported.'; return 1; }

	typeset CHOST="${SOPT[HOST]}" CPW="${SOPT[PW]}" IP=${SOPT[IP]} \
		EXE='/usr/bin/turnserver'

	[[ -z ${CPW} ]] && CPW=${ openssl rand -hex 16 ; }
	if [[ ! -e ${EXE} ]]; then
#		${DRY} adduser.broken --home / --shell /bin/false --no-create-home \
#			--disabled-password --disabled-login \
#			--gecos 'turnserver daemon' --group turnserver
		${DRY} apt-get update
		${DRY} apt-get install coturn
	fi

	typeset REALM=${CHOST#*.}

	cat >${TMP}/server.cfg<<EOF
# Verbose Mode: verbose .. moderate verbose, Verbose .. extra verbose
#verbose

# Reduce the number of relay-threads to nproc - 2
#relay-threads = 50

# Access to the admin server on port 5766
#cli-password = what_ever_you_want

# enable metrics expose via HTTP on port 9641 in prometheus exposition format
prom
# enable fine grained metrics at session instead of thread level - may get you
# into trouble - read /usr/share/doc/coturn/Metrics.md !!!
#prom-sid
# To reduce number of metrics collected/maintained be adjusting the time in
# seconds, how long a session needs to be closed before its ID can be reused.
# Default: 60. read /usr/share/doc/coturn/Metrics.md !!!
#prom-sid-retain = 60
# Log the IP address of each endpoint when a new session gets created.
log-ip

# SQLite database file name. Default: /var/lib/turn/turndb
userdb=${SOPT[DIR]}/db

# Log to a single filename (rather than new log files each startup).
log-file=/var/log/coturn.log
simple-log
new-log-timestamp
new-log-timestamp-format="%F %T.%N"

# User name to run the process. After the initialization, the turnserver process
# will attempt to change the current user ID to that user.
proc-user=${SOPT[USR]}

# Group name to run the process. After the initialization, the turnserver
# process will attempt to change the current group ID to that group.
proc-group=${SOPT[GRP]}

# These are the two network ports used by the TURN server which the client
# may connect to. We enable the standard unencrypted port 3478 for STUN,
# as well as port 443 for TURN over TLS, which can bypass firewalls.
listening-port=3478
tls-listening-port=443

# If the server has multiple IP addresses, you may wish to limit which
# addresses coturn is using. Do that by setting this option (it can be
# specified multiple times). The default is to listen on all addresses.
# You do not normally need to set this option.
#listening-ip=172.17.19.101

# If the server is behind NAT, you need to specify the external IP address.
# If there is only one external address, specify it like this:
external-ip=${IP}

# If you have multiple external addresses, you have to specify which
# internal address each corresponds to, like this. The first address is the
# external ip, and the second address is the corresponding internal IP.
#external-ip=172.17.19.131/10.0.0.11
#external-ip=172.17.18.132/10.0.0.12

# The local IP address that will be used to relay the packets to the peer.
# Multiple relay addresses may be used.
#
# If no relay IP(s) specified, default policy is used: decide itself which
# relay addresses to use, and always use the client socket IP address as the
# relay IP address of the TURN session (if the requested relay address family
# is the same as the family of the client socket).
#relay-ip=${IP}

# Lower and upper bounds of the UDP relay endpoints. Default: 49152 and 65535
min-port=49152
max-port=65535

# Per default, no peers are allowed on the loopback addresses (127.x.x.x, ::1).
# This is an extra security measure. However, for testing it might be useful.
#allow-loopback-peers

# Disallow peers on well-known broadcast addresses (224.0.0.0 and above,
# and FFXX:*). This is an extra security measure.
no-multicast-peers

# Fingerprints in TURN messages are required for WebRTC
fingerprint

# The long-term credential mechanism is required for WebRTC
lt-cred-mech

# Configure coturn to use the "TURN REST API" method for validating time-
# limited credentials. BigBlueButton will generate credentials in this
# format. Note that the static-auth-secret value specified here must match
# the configuration in BigBlueButton's turn-stun-servers.xml
# You can generate a new random value by running the command:
#   openssl rand -hex 16
use-auth-secret
static-auth-secret=${CPW}

# If the realm value is unspecified, it defaults to the TURN server hostname.
# You probably want to configure it to a domain name that you control to
# improve log output. There is no functional impact.
# realm=example.com
realm=${REALM}

# Configure TLS support.
# Adjust these paths to match the locations of your certificate files
cert=${SOPT[DIR]}/server.crt
pkey=${SOPT[DIR]}/server.key

# Limit the allowed ciphers to improve security
# Based on https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
cipher-list="ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS"
#cipher-list="ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AES:RSA+3DES:!ADH:!AECDH:!MD5"

# Enable longer DH TLS key to improve security
dh2066

# All WebRTC-compatible web browsers support TLS 1.2 or later, so disable
# older protocols
no-tlsv1
no-tlsv1_1

EOF
	X="${SOPT[DIR]}/server.cfg"
	[[ -n ${DRY} ]] && Log.info "${X}:" && cat ${TMP}/coturn.cfg
	${DRY} cp ${TMP}/server.cfg $X
	${DRY} ln -sf $X /etc/turnserver.conf
	
	[[ -e ${SOPT[DIR]}/server.crt ]] || \
		${DRY} cp ~admin/etc/server.crt ${SOPT[DIR]}/server.crt
	[[ -e ${SOPT[DIR]}/server.key ]] || \
		${DRY} cp ~admin/etc/server.key ${SOPT[DIR]}/server.key
	${DRY} chown admin:${SOPT[GRP]} ${SOPT[DIR]}/server.{crt,key} $X
	${DRY} chmod 644 ${SOPT[DIR]}/server.crt
	${DRY} chmod 640 ${SOPT[DIR]}/server.key $X

	X=/etc/logrotate.d/coturn
	cat >${TMP}/coturn.r<<EOF
/var/log/coturn.log
{
	olddir old
	daily
	sharedscripts
	postrotate
		systemctl -s HUP kill coturn.service
	endscript
}
EOF
	[[ -n ${DRY} ]] && Log.info "${X}:" && cat ${TMP}/coturn.r
	${DRY} cp ${TMP}/coturn.r $X

	# Schwachsinniges systemd: Angeblich kann man service defaults via
	# /etc/systemd/system/${svc}.service.d/*.conf überschreiben, aber sobald da
	# ExecStart= auftaucht, hat sich das erledigt. Was für ein Bullshit.
	# Ergo symlinken wir die Konfig und sind raus aus der abgefuckten Nummer.
	X='/etc/systemd/system/coturn.service.d/override.conf'
	cat >${TMP}/coturn.svc<<EOF
[Service]
# To be able to listen on port 80 and 443, we need to start the service as root
User=root
Group=root
EOF
	[[ ! -d ${X%/*} ]] && ${DRY} mkdir ${X%/*}
	${DRY} cp ${TMP}/coturn.svc $X

	X="${SOPT[DIR]}/db"
	if [[ -f /usr/share/coturn/schema.sql && ! -e ${SOPT[DIR]}/db ]]; then
		[[ -x /bin/sqlite3 ]] || apt-get install sqlite3
		sqlite3 $X </usr/share/coturn/schema.sql
	fi
	chown admin:${SOPT[GRP]} $X
	chmod 640 $X

	if [[ -e /etc/init.d/coturn ]]; then
		/sbin/update-rc.d -f coturn remove
		mv /etc/init.d/coturn /etc/init.d/OLD/ || rm -f /etc/init.d/coturn
	fi

	${DRY} systemctl stop coturn
	${DRY} systemctl daemon-reload
	${DRY} systemctl start coturn
}

function cleanup {
	[[ -z ${TMP} ]] && return 0
	rm -rf ${TMP}
}

function doMain {
	typeset -a A B

	if [[ -z ${SOPT[HOST]} ]]; then
		A=( ${ getent hosts ${ uname -n; } ; } )
		for (( I=1; i < ${#A[@]}; I++ )); do
			B=( ${A[I]//./ } )
			(( ${#B[@]} > 2 )) && SOPT[HOST]="${A[I]}" && break
		done
		if [[ -z ${SOPT[HOST]} ]]; then
			Log.fatal 'Kann den hostname dieser zone nicht bestimmen. Bitte' \
				'noch einmal mit Option -s versuchen ...'
			return 2
		fi
		[[ -z ${SOPT[IP]} ]] && SOPT[IP]=$A
	fi
	if [[ -z ${SOPT[IP]} ]]; then
		A=( ${ getent hosts ${SOPT[HOST]} ; } )
		if [[ -z $A ]]; then
			Log.fatal "Kann die IP-Adresse für '${SOPT[HOST]}'" \
				'nicht bestimmen. Evtl. mit Option -I ... versuchen.'
			return 3
		fi
		SOPT[IP]=$A
	fi
	[[ -z ${SOPT[DIR]} ]] && SOPT[DIR]=${DEFAULTS[DATADIR]}
	[[ -z ${SOPT[GRP]} ]] && SOPT[GRP]=${DEFAULTS[GRP]}
	[[ -z ${SOPT[USR]} ]] && SOPT[USR]=${DEFAULTS[USR]}
	[[ ! -d ${SOPT[DIR]} ]] && { mkdir -p ${SOPT[DIR]} || return 4 ; }

	Log.info "Nutze folgende Parameter:\n\thostname:    '${SOPT[HOST]}'" \
		"\n\tExterne IP:  '${SOPT[IP]}'"
	TMP=${ mktemp -td coturn.XXXXXX ; }
	[[ -z ${TMP} ]] && Log.fatal 'No temp dir - exiting.' && return 5

	installCoturn
}

Man.addFunc MAIN '' '[+NAME?'"${PROG}"' - Coturn server setup.]
[+DESCRIPTION?Das Skript dient der Installation von Coturn, einem STUN und TURN server. Letzterer kann genutzt werden, um UDP via HTTPS zu tunneln, was z.B. für WebRTC notwendig sein kann.]
[h:help?Diese Hilfe ausgegeben und Script beenden.]
[F:functions?Liste aller definierten Funktionen im Script ausgeben (\btypeset +f\b).]
[H:usage]:[function?Hilfe für die angegebene Script-Funktion bzw. Objekttyp anzeigen (soweit verfügbar) und Script beenden. Siehe auch Option \b-F\b.]
[T:trace]:[fname_list?Eine Komma- oder Leerzeichen-separierte Liste von Funktionsnamen, die während ihrer Ausführung getracet werden sollen.]
[n:dry?Nur zeigen, was gemacht werden würde aber nicht wirklich irgende Änderung am System vornehmen.]
[+?]
[d:datadir]:[path?Use the given \apath\a to store the config file, user database and certificate incl. key. Default: '"${DEFAULTS[DATADIR]}"']
[g:group]:[name?Run the server using the GID of group \aname\a after initialization. Default: '"${DEFAULTS[GRP]}"']
[i:ip]:[addr?Die externe IP-Adresse dieses Servers. Wird automatisch bestimmt, falls nicht angeggeben (siehe auch Option -s ...).]
[s:host]:[fqdn?Nutze den angegebenen \afqdn\a als von \bextern\b zu benutzenden FQDN. Wird automatisch vom nodename der Zone abgeleitet, falls nicht angegeben.]
[p:pw]:[secret?Dienst mit Paßwort \asecret\a ansichern. Alle Dienste (wie z.B. BBB) die darüber ihre Dienste zugänglich machen, müssen diesen \asecret\a kennen/konfiguriert haben. Default: \aauto-generated\a]
[u:user]:[name?Run the server using the UID of user \aname\a after initialization. Default: '"${DEFAULTS[USR]}"']
'
unset SOPT DRY TMP; typeset -A SOPT
typeset TMP
typeset X="${ print ${Man.FUNC[MAIN]} ; }"

while getopts "${X}" option ; do
	case "${option}" in
		h) showUsage MAIN ; exit 0 ;;
		F) typeset +f ; exit 0 ;;
		H)  if [[ ${OPTARG%_t} != $OPTARG ]]; then
				${OPTARG} --man   # self-defined types
			else
				showUsage "${OPTARG}"   # function
			fi
			exit 0
			;;
		T)	if [[ ${OPTARG} == 'ALL' ]]; then
				typeset -ft ${ typeset +f ; }
			else
				typeset -ft ${OPTARG//,/ }
			fi
			;;
		n) DRY='print --' ;;

		d) SOPT[DIR]="${OPTARG}" ;;
		g) SOPT[GRP]="${OPTARG}" ;;
		i) SOPT[IP]="${OPTARG}" ;;
		s) SOPT[HOST]="${OPTARG}" ;;
		p) SOPT[PW]="${OPTARG}" ;;
		u) SOPT[USR]="${OPTARG}" ;;
		*) showUsage ;;
	esac
done
X=$((OPTIND-1))
shift $X

trap cleanup EXIT

doMain "$@"
