summaryrefslogtreecommitdiff
path: root/src/chroot-tools/distcc-tool
blob: 763302915c6099d177724a8e44184afb35b68820 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
#!/usr/bin/env bash
# -*- tab-width: 4; sh-basic-offset: 4 -*-
# distcc-tool

# Copyright 2013 Luke Shumaker
#
# 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: I don't know what version, I use fairly modern features
#  - socat
#  - cat: any version
#  - rm: any version
#  - sed: any version
# On Parabola, this means the packages:
#  bash, coreutils, sed, socat

if ! type gettext &>/dev/null; then
	gettext() { echo "$@"; }
fi

panic() {
	echo "$(gettext 'panic: malformed call to internal function')" >&2
	exit 1
}

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

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

usage() {
	print "Usage: $0 COMMAND [COMMAND-ARGS]"
	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="$(gettext "$1")"; shift
		printf "$(gettext '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 pids=() # child pids
	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 used `client`.
				newhosts+=("$HOSTSPEC")
				;;
			# GLOBAL_OPTION
			--*)
				# pass these through
				newhosts+=("$HOSTSPEC")
				;;
			# ZEROCONF
			+zeroconf)
				error "%s 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},fork SYSTEM:"$0 client $HOSTID ${PORT:-3632}" &
					pids+=($!)
				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 [[ -z "${pids[*]}" ]]; then
			# listen on port 8000, but immediatly close, just so that we are
			# listening on something
			socat TCP-LISTEN:${newport},fork SYSTEM:true &
			pids+=($!)
		fi
		trap "kill -- ${pids[*]}" EXIT
		wait "${pids[@]}"
	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
	while read -r host port; do
		socat STDIO TCP:"$host:$port"
		break
	done
}

# 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
	local file=/socket
	{ printf '%s\n' "$*"; cat; } | socat UNIX-CONNECT:"$file" 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

	umask 111
	socat UNIX-LISTEN:"$chrootpath/socket",fork SYSTEM:"$0 server" &
	trap "kill -- $!; rm -f '$chrootpath/socket'" EXIT
	wait
}

# 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 "$@"