1#!/bin/sh
2# r2docker
3# ========
4#
5# Requires ~140MB of free disk space
6#
7# Build docker image with:
8# $ ./sys/docker_build_alpine_image.sh
9#
10# Run the docker image:
11#
12# $ r2d() {
13#	local rm
14#	case "$1" in
15#	"-r"|"--rm") rm="--rm" && shift ;;
16#	esac
17#	docker run --cap-drop=ALL --cap-add=SYS_PTRACE -i \
18#		`--name r2_$(date +%F_%H%M%S%N) $rm -tv $(pwd):/r2 \
19#		r2_alpine:latest $@
20# }
21# $ r2d # Optional --rm
22#
23# Once you quit the session, get the container id with something like:
24#
25# $ containerid="$(docker ps -a | awk '/r2_alpine/ {print $NF}')"
26#
27# To get into that shell again just type:
28#
29# $ docker start -ai $containerid
30#
31# To share those images:
32#
33# $ docker export $containerid | xz >container.tar.xz
34# $ xz -d <container.tar.xz | docker import -
35#
36# When finished:
37#
38# $ docker rm -f $containerid
39#
40# If you are unwilling to debug a program within Docker, remove
41# --cap-add=SYS_PTRACE from the r2d function. If you need sudo to
42# install more packages within Docker, remove --cap-drop=ALL from the
43# r2d function.
44
45### Helpers begin
46checkdeps() {
47	for d in curl docker jq; do
48		if [ -z "$(command -v "$d")" ]; then
49			echo "[!] $d is not installed"
50			exit 128
51		fi
52	done
53	unset d
54}
55long_opt() {
56	arg=""
57	shift="0"
58	case "$1" in
59	"--"*"="*) arg="${1#*=}"; [ -n "$arg" ] || usage 127 ;;
60	*) shift="1"; shift; [ $# -gt 0 ] || usage 127; arg="$1" ;;
61	esac
62	echo "$arg"
63	unset arg
64	return $shift
65}
66### Helpers end
67
68cleanup() {
69	rm -rf "$tmp_docker_dir"
70	[ "${1:-0}" -eq 0 ] || exit "$1"
71}
72
73usage() {
74	cat <<EOF
75Usage: ${0##*/} [OPTIONS]
76
77Build a Radare2 docker image that uses local user/group IDs.
78
79Options:
80    -b, --branch=BRANCH    Use specified radare2 branch (default:
81                           master)
82    -h, --help             Display this help message
83    --node=VERSION         Use specified Node.js version (default: 10)
84    -n, --npm=VERSION      Use specified version of r2pipe npm binding
85                           (default: newest)
86    -p, --py=VERSION       Use specified version of r2pipe python
87                           binding (default: newest)
88
89EOF
90	exit "$1"
91}
92
93args=""
94unset help r2pipe_npm r2pipe_py
95github="https://github.com/radareorg/radare2.git"
96r2branch="master"
97
98# Check for missing dependencies
99checkdeps
100
101# Parse command line options
102while [ $# -gt 0 ]; do
103	case "$1" in
104	"--") shift && args="$args${args:+ }$*" && break ;;
105	"-b"|"--branch"*) r2branch="$(long_opt "$@")" || shift ;;
106	"-h"|"--help") help="true" ;;
107	"-n"|"--npm"*) r2pipe_npm="@$(long_opt "$@")" || shift ;;
108	"-p"|"--py"*) r2pipe_py="==$(long_opt "$@")" || shift ;;
109	*) args="$args${args:+ }$1" ;;
110	esac
111	shift
112done
113[ -z "$args" ] || set -- "$args"
114
115# Check for valid params
116[ -z "$help" ] || usage 0
117[ $# -eq 0 ] || usage 1
118if [ "`uname`" = Linux ]; then
119	if [ "$(id -u)" -ne 0 ] && id -Gn | grep -qvw "docker"; then
120		echo "[!] You are not part of the docker group"
121		exit 2
122	fi
123fi
124
125trap "cleanup 126" INT
126
127# Create Dockerfile
128tmp_docker_dir=".docker_alpine"
129gid="$(id -g)"
130gname="$(id -gn)"
131r2commit="$(
132	curl -Ls \
133	"http://api.github.com/repos/radare/radare2/commits/$r2branch" | \
134	jq -cMrS ".sha"
135)"
136uid="$(id -u)"
137uname="$(id -nu)"
138mkdir -p $tmp_docker_dir
139cat >$tmp_docker_dir/Dockerfile <<EOF
140# Using super tiny alpine base image
141FROM alpine:latest
142
143# Install bash b/c it's better
144RUN apk upgrade && apk add bash
145
146# Bash is better than sh
147SHELL ["/bin/bash", "-c"]
148
149# Build radare2 in a volume to minimize space used by build
150VOLUME ["/mnt"]
151
152# All one RUN layer, splitting into 3 increases size to ~360MB, wtf
153# 1. Install dependencies
154# 2. Install Node.js and Python bindings (do we need these?)
155# 3. Create id and gid to match current local user
156# 4. Add new user to sudoers without password
157# 5. Add some convenient aliases to .bashrc
158# 6. Clone and install radare2
159# 7. Clean up unnecessary files and packages
160RUN set -o pipefail && \
161	( \
162		apk upgrade && \
163		apk add \
164			g++ \
165			gcc \
166			git \
167			libc6-compat \
168			linux-headers \
169			make \
170			ncurses-libs \
171			nodejs-current \
172			npm \
173			py2-pip \
174			shadow \
175			sudo \
176	) && ( \
177		npm install -g --unsafe-perm "r2pipe$r2pipe_npm" && \
178		pip install --upgrade pip && \
179		pip install r2pipe$r2pipe_py \
180	) && ( \
181		[ "$gname" != "root" ] || \
182		( \
183			echo "alias la=\"\\ls -AF\"" >>/root/.bashrc && \
184			echo "alias ll=\"\\ls -Fhl\"" >>/root/.bashrc && \
185			echo "alias ls=\"\\ls -F\"" >>/root/.bashrc && \
186			echo "alias q=\"exit\"" >>/root/.bashrc \
187		) \
188	) && ( \
189		[ "$gname" = "root" ] || \
190		( \
191			groupadd -f $gname && \
192			(groupmod -g $gid $gname 2>/dev/null || true) && \
193			useradd -g $gid -mou $uid $uname && \
194			echo "$uname ALL=(ALL) NOPASSWD: ALL" \
195				>/etc/sudoers.d/$uname && \
196			echo "alias la=\"\\ls -AF\"" >>/home/$uname/.bashrc && \
197			echo "alias ll=\"\\ls -Fhl\"" >>/home/$uname/.bashrc && \
198			echo "alias ls=\"\\ls -F\"" >>/home/$uname/.bashrc && \
199			echo "alias q=\"exit\"" >>/home/$uname/.bashrc \
200		) \
201	) && ( \
202		cd /mnt && \
203		git clone -b $r2branch --depth 1 $github && \
204		cd radare2 && \
205		git checkout $r2commit && \
206		./sys/install.sh && \
207		make install \
208	) && ( \
209		apk del --purge -r \
210			g++ \
211			gcc \
212			linux-headers \
213			make \
214			npm \
215			py2-pip && \
216		rm -rf /tmp/* /var/cache/apk/* /var/tmp/* \
217	)
218
219# Initialize env
220USER $(id -nu)
221WORKDIR /r2
222
223# Setup r2pm
224RUN set -o pipefail && \
225	r2pm init && \
226	r2pm update
227
228CMD ["/bin/bash"]
229EOF
230
231# Tag old images
232echo "[*] Tagging any old r2_alpine images"
233docker images | awk '{print $1":"$3}' | while read -r tag; do
234	case "$tag" in
235	"r2_alpine:"*) docker image tag "${tag#*:}" "$tag" ;;
236	esac
237done
238unset tag
239
240findbase="$(
241	docker images | grep -E "^(docker\.io\/)?alpine +latest "
242)"
243
244# Build image (may take a while)
245echo "[*] Building image..."
246echo "[*] This may take a long time..."
247
248# Pull newest base image and build r2_alpine image
249docker pull alpine:latest
250(
251	if [ ! -d "$tmp_docker_dir" ]; then
252		echo "$tmp_docker_dir not found"
253	fi
254	cd $tmp_docker_dir || exit 3
255	# shellcheck disable=SC2154
256	docker build \
257		${http_proxy:+--build-arg http_proxy=$http_proxy} \
258		${https_proxy:+--build-arg https_proxy=$https_proxy} \
259		-t r2_alpine:latest .
260)
261
262# Only remove base image if it didn't already exist
263[ -n "$findbase" ] || docker rmi alpine:latest
264
265echo "[*] done"
266
267old_base="^(docker\.io\/)?alpine +<none>"
268old_r2="^r2_alpine +[^l ]"
269found="$(
270	docker images | grep -E "($old_base)|($old_r2)"
271)"
272if [ -n "$found" ]; then
273	# List old images
274	echo
275	echo "[*] Old images:"
276	docker images | head -n 1
277
278	docker images | grep -E "($old_base)|($old_r2)" | \
279		while read -r line; do
280		echo "$line"
281	done
282	unset line
283
284	# Prompt to remove old images
285	unset remove
286	echo
287	while :; do
288		echo "Remove old images (y/N/q)?"
289		read -r ans
290		echo
291		case "$ans" in
292		""|"n"|"N"|"q"|"Q") break ;;
293		"y"|"Y") remove="true"; break ;;
294		*) echo "Invalid choice" ;;
295		esac
296	done
297
298	if [ -n "$remove" ]; then
299		# Remove old images
300		docker images | awk "/$old_r2/ {print \$1\":\"\$3}" | \
301		while read -r tag; do
302			docker rmi "$tag"
303		done
304		unset tag
305
306		docker images | awk "/$old_base/ {print \$3}" | \
307		while read -r id; do
308			docker rmi "$id"
309		done
310		unset id
311	fi
312fi
313unset found
314
315cleanup 0
316
317cat <<EOF
318
319It's suggested you add something like the following to your ~/.bashrc:
320
321r2d() {
322	local rm
323	case "\$1" in
324	"-r"|"--rm") rm="--rm" && shift ;;
325	esac
326	docker run --cap-drop=ALL --cap-add=SYS_PTRACE -i \\
327		--name r2_\$(date +%F_%H%M%S%N) \$rm -tv \$(pwd):/r2 \\
328		r2_alpine:latest \$@
329}
330EOF
331