#!/usr/bin/env bash
# distcc-tool

# Copyright (C) 2013-2014, 2017-2018, 2024 Luke T. Shumaker <lukeshu@parabola.nu>
#
# License: GNU GPLv3+
#
# This file is part of Parabola.
#
# Parabola is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Parabola is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Parabola. If not, see <http://www.gnu.org/licenses/>.

# This program has very few dependencies:
#  - bash: 4.4 or newer (for @Q escaping)
#  - socat
#  - sleep: must accept "infinity"
#  - cat: any version
#  - rm: any version
#  - sed: any version
# On Parabola, this means the packages:
#  bash, coreutils, sed, socat

export TEXTDOMAIN='libretools'

if type gettext &>/dev/null; then
	_() { gettext "$@"; }
else
	_() { echo -n "$@"; }
fi

panic() {
	print 'panic: malformed call to internal function' >&2
	exit 1
}

error() {
	local mesg
	mesg="$(_ "$1")"
	shift
	printf -- "$(_ 'ERROR:') $mesg\n" "$@" >&2
	exit 1
}

print() {
	local mesg
	mesg="$(_ "$1")"
	shift
	printf -- "$mesg\n" "$@"
}

usage() {
	print "Usage: %s COMMAND [COMMAND-ARGS]" "$0"
	print "Tool for using distcc within a networkless chroot."
	echo
	print "Commands:"
	print '  help                  print this message'
	print '  odaemon CHROOTPATH    daemon to run outside of the chroot'
	print '  idaemon DISTCC_HOSTS  daemon to run inside of the chroot'
	print '  rewrite DISTCC_HOSTS  prints a rewritten version of DISTCC_HOSTS'
	print '  client HOST PORT      connects stdio to TCP:$HOST:$PORT'
	print 'Commands: for internal use'
	print '  server                counterpart to client; spawned by odaemon'
}

errusage() {
	if [[ $# -gt 0 ]]; then
		fmt="$(_ "$1")"
		shift
		printf "$(_ 'ERROR:') $fmt\n" "$@" >&2
	fi
	usage >&2
	exit 1
}

main() {
	local cmd=$1
	shift
	case "$cmd" in
		help)
			[[ $# -eq 0 ]] || errusage '%s: invalid number of arguments' "$cmd"
			usage
			;;
		odaemon | idaemon | rewrite)
			[[ $# -eq 1 ]] || errusage '%s: invalid number of arguments' "$cmd"
			$cmd "$@"
			;;
		client)
			[[ $# -eq 2 ]] || errusage '%s: invalid number of arguments' "$cmd"
			$cmd "$@"
			;;
		server)
			[[ $# -eq 0 ]] || errusage '%s: invalid number of arguments' "$cmd"
			$cmd "$@"
			;;
		*) errusage 'unknown subcommand: %s' "$cmd" ;;
	esac
}

################################################################################
# DISTCC_HOSTS parser                                                          #
################################################################################

# usage: parse_DISTCC_HOSTS true|false <DISTCC_HOSTS>
# parses <DISTCC_HOSTS> and:
# $1==true : It sets up port forwarding for inside the choot, sleep forever
# $1==false: Prints a modified version of DISTCC_HOSTS that uses the forwarded
#            ports that were set up when $1==true.
parse_DISTCC_HOSTS() {
	{ [[ $# -eq 2 ]] && { [[ $1 == true ]] || [[ $1 == false ]]; }; } || panic
	local forward_ports=$1
	local DISTCC_HOSTS=$2

	local newhosts=()
	local newport=8000 # next port to be used for port forwarding

	# This is based on the grammar specified in distcc(1)
	local HOSTSPEC
	for HOSTSPEC in $(sed 's/#.*//' <<<"$DISTCC_HOSTS"); do
		case "$HOSTSPEC" in
			# LOCAL_HOST
			localhost | localhost/* | --localslots=* | --localslots_cpp=*)
				# "localhost" runs commands directly, not talking to distccd at
				# localhost, use an IP or real hostname for that.
				# So, just pass these through.
				newhosts+=("$HOSTSPEC")
				;;
			# SSH_HOST
			*@*)
				# SSH_HOST doesn't allow custom port numbers, and even if it
				# did, ssh would complain about MITM.  Instead, we'll count on
				# ssh ProxyCommand being configured to use `client`.
				newhosts+=("$HOSTSPEC")
				;;
			# GLOBAL_OPTION
			--*)
				# pass these through
				newhosts+=("$HOSTSPEC")
				;;
			# ZEROCONF
			+zeroconf)
				error "%q does not support the +zeroconf option" "$0"
				exit 1
				;;
			# TCP_HOST or OLDSTYLE_TCP_HOST
			*)
				declare HOSTID='' PORT='' LIMIT='' OPTIONS=''
				if [[ $HOSTSPEC =~ ^([^:/]+)(:([0-9]+))?(/([0-9]+))?(,.*)?$ ]]; then
					# TCP_HOST
					HOSTID=${BASH_REMATCH[1]}
					PORT=${BASH_REMATCH[3]}
					LIMIT=${BASH_REMATCH[5]}
					OPTIONS=${BASH_REMATCH[6]}
				elif [[ $HOSTSPEC =~ ^([^:/]+)(/([0-9]+))?(:([0-9]+))?(,.*)?$ ]]; then
					# OLDSTYLE_TCP_HOST
					HOSTID=${BASH_REMATCH[1]}
					LIMIT=${BASH_REMATCH[3]}
					PORT=${BASH_REMATCH[5]}
					OPTIONS=${BASH_REMATCH[6]}
				else
					error "Could not parse HOSTSPEC: %s" "$HOSTSPEC"
				fi

				# set up port forwaring
				if $forward_ports; then
					socat TCP-LISTEN:${newport},reuseaddr,fork SYSTEM:"${0@Q} client $HOSTID ${PORT:-3632}" &
				fi

				# add the forwarded port
				local newhost="127.0.0.1:$newport"
				[[ -z $LIMIT ]] || newhost+="/$LIMIT"
				[[ -z $OPTIONS ]] || newhost+="$OPTIONS"
				newhosts+=("$newhost")
				: $((newport++))
				;;
		esac
	done
	if $forward_ports; then
		if [[ $newport == 8000 ]]; then
			exec sleep infinity
		fi
		trap "jobs -p | xargs -r kill --" EXIT
		wait
	else
		printf '%s\n' "${newhosts[*]}"
	fi
}

################################################################################
# Port forwarding primitives                                                   #
################################################################################

# Usage: server
# Reads "host port" from the first line of stdin, then connects stdio to the
# specified TCP socket.
server() {
	[[ $# -eq 0 ]] || panic
	local host port
	read -r host port
	exec socat STDIO TCP:"$host:$port"
}

# Usage: client HOST PORT
# For usage inside of a chroot. It talks through the UNIX-domain socket to an
# instance of `server` outside, in order to connect stdio to the specified TCP
# socket.
client() {
	[[ $# -eq 2 ]] || panic
	{
		printf '%s\n' "$*"
		exec cat
	} | exec socat UNIX-CONNECT:'/socket' STDIO
}

################################################################################
# High-level routines                                                          #
################################################################################

# Usage: odaemon CHROOTPATH
# Listens on "$CHROOTPATH/socket" and spawns a `server` for each new connection.
odaemon() {
	[[ $# -eq 1 ]] || panic
	local chrootpath=$1

	cd "$chrootpath"
	umask 111
	exec socat UNIX-LISTEN:'./socket',fork SYSTEM:"${0@Q} server"
}

# Usage: idaemon DISTCC_HOSTS
# Sets things up inside of the chroot to forward distcc hosts out.
idaemon() {
	[[ $# -eq 1 ]] || panic
	parse_DISTCC_HOSTS true "$1"
}

# Usage: rewrite DISTCC_HOSTS
# Prints a modified version of DISTCC_HOSTS for inside the chroot.
rewrite() {
	[[ $# -eq 1 ]] || panic
	parse_DISTCC_HOSTS false "$1"
}

main "$@"
