1#
2# Copyright (c) 2023 Klara, Inc.
3#
4# SPDX-License-Identifier: BSD-2-Clause
5#
6
7tftp_dir="${TMPDIR:-/tmp}/tftp.dir"
8inetd_conf="${TMPDIR:-/tmp}/inetd.conf"
9inetd_pid="${TMPDIR:-/tmp}/inetd.pid"
10
11start_tftpd() {
12	if ! [ -z "$(sockstat -PUDP -p69 -q)" ] ; then
13		atf_skip "the tftp port is in use"
14	fi
15	echo "starting inetd for $(atf_get ident)" >&2
16	rm -rf "${tftp_dir}"
17	mkdir "${tftp_dir}"
18	cat >"${inetd_conf}" <<EOF
19tftp	dgram	udp	wait	root	/usr/libexec/tftpd	tftpd -d15 -l ${tftp_dir}
20tftp	dgram	udp6	wait	root	/usr/libexec/tftpd	tftpd -d15 -l ${tftp_dir}
21EOF
22	/usr/sbin/inetd -a localhost -p "${inetd_pid}" "${inetd_conf}"
23}
24
25stop_tftpd() {
26	echo "stopping inetd for $(atf_get ident)" >&2
27	# Send SIGTERM to inetd, then SIGKILL until it's gone
28	local sig=TERM
29	while pkill -$sig -LF "${inetd_pid}" inetd ; do
30		echo "waiting for inetd to stop" >&2
31		sleep 1
32		sig=KILL
33	done
34	rm -rf "${tftp_dir}" "${inetd_conf}" "${inetd_pid}"
35}
36
37atf_test_case tftp_get_big cleanup
38tftp_get_big_head() {
39	atf_set "descr" "get command with big file"
40	atf_set "require.user" "root"
41}
42tftp_get_big_body() {
43	start_tftpd
44	local remote_file="${tftp_dir}/remote.bin"
45	dd if=/dev/urandom of="${remote_file}" bs=1m count=16 status=none
46	local local_file="local.bin"
47	echo "get ${remote_file##*/} ${local_file}" >client-script
48	atf_check -o match:"Received [0-9]+ bytes" \
49	    tftp localhost <client-script
50	atf_check cmp -s "${local_file}" "${remote_file}"
51}
52tftp_get_big_cleanup() {
53	stop_tftpd
54}
55
56atf_test_case tftp_get_host cleanup
57tftp_get_host_head() {
58	atf_set "descr" "get command with host name"
59	atf_set "require.user" "root"
60}
61tftp_get_host_body() {
62	start_tftpd
63	local remote_file="${tftp_dir}/hello.txt"
64	echo "Hello, $$!" >"${remote_file}"
65	local local_file="${remote_file##*/}"
66	echo "get localhost:${remote_file##*/}" >client-script
67	atf_check -o match:"Received [0-9]+ bytes" \
68	    tftp <client-script
69	atf_check cmp -s "${local_file}" "${remote_file}"
70}
71tftp_get_host_cleanup() {
72	stop_tftpd
73}
74
75atf_test_case tftp_get_ipv4 cleanup
76tftp_get_ipv4_head() {
77	atf_set "descr" "get command with ipv4 address"
78	atf_set "require.user" "root"
79}
80tftp_get_ipv4_body() {
81	start_tftpd
82	local remote_file="${tftp_dir}/hello.txt"
83	echo "Hello, $$!" >"${remote_file}"
84	local local_file="${remote_file##*/}"
85	echo "get 127.0.0.1:${remote_file##*/}" >client-script
86	atf_check -o match:"Received [0-9]+ bytes" \
87	    tftp <client-script
88	atf_check cmp -s "${local_file}" "${remote_file}"
89}
90tftp_get_ipv4_cleanup() {
91	stop_tftpd
92}
93
94atf_test_case tftp_get_ipv6 cleanup
95tftp_get_ipv6_head() {
96	atf_set "descr" "get command with ipv6 address"
97	atf_set "require.user" "root"
98}
99tftp_get_ipv6_body() {
100	sysctl -q kern.features.inet6 || atf_skip "This test requires IPv6 support"
101	start_tftpd
102	local remote_file="${tftp_dir}/hello.txt"
103	echo "Hello, $$!" >"${remote_file}"
104	local local_file="${remote_file##*/}"
105	echo "get [::1]:${remote_file##*/}" >client-script
106	atf_check -o match:"Received [0-9]+ bytes" \
107	    tftp <client-script
108	atf_check cmp -s "${local_file}" "${remote_file}"
109}
110tftp_get_ipv6_cleanup() {
111	stop_tftpd
112}
113
114atf_test_case tftp_get_one cleanup
115tftp_get_one_head() {
116	atf_set "descr" "get command with one argument"
117	atf_set "require.user" "root"
118}
119tftp_get_one_body() {
120	start_tftpd
121	local remote_file="${tftp_dir}/hello.txt"
122	echo "Hello, $$!" >"${remote_file}"
123	local local_file="${remote_file##*/}"
124	echo "get ${remote_file##*/}" >client-script
125	atf_check -o match:"Received [0-9]+ bytes" \
126	    tftp localhost <client-script
127	atf_check cmp -s "${local_file}" "${remote_file}"
128}
129tftp_get_one_cleanup() {
130	stop_tftpd
131}
132
133atf_test_case tftp_get_two cleanup
134tftp_get_two_head() {
135	atf_set "descr" "get command with two arguments"
136	atf_set "require.user" "root"
137}
138tftp_get_two_body() {
139	start_tftpd
140	local remote_file="${tftp_dir}/hello.txt"
141	echo "Hello, $$!" >"${remote_file}"
142	local local_file="world.txt"
143	echo "get ${remote_file##*/} ${local_file}" >client-script
144	atf_check -o match:"Received [0-9]+ bytes" \
145	    tftp localhost <client-script
146	atf_check cmp -s "${local_file}" "${remote_file}"
147}
148tftp_get_two_cleanup() {
149	stop_tftpd
150}
151
152atf_test_case tftp_get_more cleanup
153tftp_get_more_head() {
154	atf_set "descr" "get command with three or more arguments"
155	atf_set "require.user" "root"
156}
157tftp_get_more_body() {
158	start_tftpd
159	for n in 3 4 5 6 7 ; do
160		echo -n "get" >client-script
161		for f in $(jot -c $n 97) ; do
162			echo "test file $$/$f/$n" >"${tftp_dir}/${f}.txt"
163			echo -n " ${f}.txt" >>client-script
164			rm -f "${f}.txt"
165		done
166		echo >>client-script
167		atf_check -o match:"Received [0-9]+ bytes" \
168		    tftp localhost <client-script
169		for f in $(jot -c $n 97) ; do
170			atf_check cmp -s "${f}.txt" "${tftp_dir}/${f}.txt"
171		done
172	done
173}
174tftp_get_more_cleanup() {
175	stop_tftpd
176}
177
178atf_test_case tftp_get_multi_host cleanup
179tftp_get_multi_host_head() {
180	atf_set "descr" "get command with multiple files and host name"
181	atf_set "require.user" "root"
182}
183tftp_get_multi_host_body() {
184	start_tftpd
185	for f in a b c ; do
186		echo "test file $$/$f/$n" >"${tftp_dir}/${f}.txt"
187		rm -f "${f}.txt"
188	done
189	echo "get localhost:a.txt b.txt c.txt" >client-script
190	atf_check -o match:"Received [0-9]+ bytes" \
191	    tftp localhost <client-script
192	for f in a b c ; do
193		atf_check cmp -s "${f}.txt" "${tftp_dir}/${f}.txt"
194	done
195}
196tftp_get_multi_host_cleanup() {
197	stop_tftpd
198}
199
200atf_test_case tftp_put_big cleanup
201tftp_put_big_head() {
202	atf_set "descr" "put command with big file"
203	atf_set "require.user" "root"
204}
205tftp_put_big_body() {
206	start_tftpd
207	local local_file="local.bin"
208	dd if=/dev/urandom of="${local_file}" bs=1m count=16 status=none
209	local remote_file="${tftp_dir}/random.bin"
210	truncate -s 0 "${remote_file}"
211	chown nobody:nogroup "${remote_file}"
212	chmod 0666 "${remote_file}"
213	echo "put ${local_file} ${remote_file##*/}" >client-script
214	atf_check -o match:"Sent [0-9]+ bytes" \
215	    tftp localhost <client-script
216	atf_check cmp -s "${remote_file}" "${local_file}"
217}
218tftp_put_big_cleanup() {
219	stop_tftpd
220}
221
222atf_test_case tftp_put_host cleanup
223tftp_put_host_head() {
224	atf_set "descr" "put command with host name"
225	atf_set "require.user" "root"
226}
227tftp_put_host_body() {
228	start_tftpd
229	local local_file="local.txt"
230	echo "test file $$" >"${local_file}"
231	local remote_file="${tftp_dir}/remote.txt"
232	truncate -s 0 "${remote_file}"
233	chown nobody:nogroup "${remote_file}"
234	chmod 0666 "${remote_file}"
235	echo "put ${local_file} localhost:${remote_file##*/}" >client-script
236	atf_check -o match:"Sent [0-9]+ bytes" \
237	    tftp <client-script
238	atf_check cmp -s "${remote_file}" "${local_file}"
239}
240tftp_put_host_cleanup() {
241	stop_tftpd
242}
243
244atf_test_case tftp_put_ipv4 cleanup
245tftp_put_ipv4_head() {
246	atf_set "descr" "put command with ipv4 address"
247	atf_set "require.user" "root"
248}
249tftp_put_ipv4_body() {
250	start_tftpd
251	local local_file="local.txt"
252	echo "test file $$" >"${local_file}"
253	local remote_file="${tftp_dir}/remote.txt"
254	truncate -s 0 "${remote_file}"
255	chown nobody:nogroup "${remote_file}"
256	chmod 0666 "${remote_file}"
257	echo "put ${local_file} 127.0.0.1:${remote_file##*/}" >client-script
258	atf_check -o match:"Sent [0-9]+ bytes" \
259	    tftp <client-script
260	atf_check cmp -s "${remote_file}" "${local_file}"
261}
262tftp_put_ipv4_cleanup() {
263	stop_tftpd
264}
265
266atf_test_case tftp_put_ipv6 cleanup
267tftp_put_ipv6_head() {
268	atf_set "descr" "put command with ipv6 address"
269	atf_set "require.user" "root"
270}
271tftp_put_ipv6_body() {
272	sysctl -q kern.features.inet6 || atf_skip "This test requires IPv6 support"
273	start_tftpd
274	local local_file="local.txt"
275	echo "test file $$" >"${local_file}"
276	local remote_file="${tftp_dir}/remote.txt"
277	truncate -s 0 "${remote_file}"
278	chown nobody:nogroup "${remote_file}"
279	chmod 0666 "${remote_file}"
280	echo "put ${local_file} [::1]:${remote_file##*/}" >client-script
281	atf_check -o match:"Sent [0-9]+ bytes" \
282	    tftp <client-script
283	atf_check cmp -s "${remote_file}" "${local_file}"
284}
285tftp_put_ipv6_cleanup() {
286	stop_tftpd
287}
288
289atf_test_case tftp_put_one cleanup
290tftp_put_one_head() {
291	atf_set "descr" "put command with one argument"
292	atf_set "require.user" "root"
293}
294tftp_put_one_body() {
295	start_tftpd
296	local local_file="file.txt"
297	echo "test file $$" >"${local_file}"
298	local remote_file="${tftp_dir}/${local_file}"
299	truncate -s 0 "${remote_file}"
300	chown nobody:nogroup "${remote_file}"
301	chmod 0666 "${remote_file}"
302	echo "put ${local_file}" >client-script
303	atf_check -o match:"Sent [0-9]+ bytes" \
304	    tftp localhost <client-script
305	atf_check cmp -s "${remote_file}" "${local_file}"
306}
307tftp_put_one_cleanup() {
308	stop_tftpd
309}
310
311atf_test_case tftp_put_two cleanup
312tftp_put_two_head() {
313	atf_set "descr" "put command with two arguments"
314	atf_set "require.user" "root"
315}
316tftp_put_two_body() {
317	start_tftpd
318	local local_file="local.txt"
319	echo "test file $$" >"${local_file}"
320	local remote_file="${tftp_dir}/remote.txt"
321	truncate -s 0 "${remote_file}"
322	chown nobody:nogroup "${remote_file}"
323	chmod 0666 "${remote_file}"
324	echo "put ${local_file} ${remote_file##*/}" >client-script
325	atf_check -o match:"Sent [0-9]+ bytes" \
326	    tftp localhost <client-script
327	atf_check cmp -s "${remote_file}" "${local_file}"
328}
329tftp_put_two_cleanup() {
330	stop_tftpd
331}
332
333atf_test_case tftp_put_more cleanup
334tftp_put_more_head() {
335	atf_set "descr" "put command with three or more arguments"
336	atf_set "require.user" "root"
337}
338tftp_put_more_body() {
339	start_tftpd
340	mkdir "${tftp_dir}/subdir"
341	for n in 2 3 4 5 6 ; do
342		echo -n "put" >client-script
343		for f in $(jot -c $n 97) ; do
344			echo "test file $$/$f/$n" >"${f}.txt"
345			truncate -s 0 "${tftp_dir}/subdir/${f}.txt"
346			chown nobody:nogroup "${tftp_dir}/subdir/${f}.txt"
347			chmod 0666 "${tftp_dir}/subdir/${f}.txt"
348			echo -n " ${f}.txt" >>client-script
349		done
350		echo " subdir" >>client-script
351		atf_check -o match:"Sent [0-9]+ bytes" \
352		    tftp localhost <client-script
353		for f in $(jot -c $n 97) ; do
354			atf_check cmp -s "${tftp_dir}/subdir/${f}.txt" "${f}.txt"
355		done
356	done
357}
358tftp_put_more_cleanup() {
359	stop_tftpd
360}
361
362atf_test_case tftp_put_multi_host cleanup
363tftp_put_multi_host_head() {
364	atf_set "descr" "put command with multiple files and host name"
365	atf_set "require.user" "root"
366}
367tftp_put_multi_host_body() {
368	start_tftpd
369	mkdir "${tftp_dir}/subdir"
370	echo -n "put" >client-script
371	for f in a b c ; do
372		echo "test file $$/$f" >"${f}.txt"
373		truncate -s 0 "${tftp_dir}/subdir/${f}.txt"
374		chown nobody:nogroup "${tftp_dir}/subdir/${f}.txt"
375		chmod 0666 "${tftp_dir}/subdir/${f}.txt"
376		echo -n " ${f}.txt" >>client-script
377	done
378	echo " localhost:subdir" >>client-script
379	atf_check -o match:"Sent [0-9]+ bytes" \
380	    tftp <client-script
381	for f in a b c ; do
382		atf_check cmp -s "${tftp_dir}/subdir/${f}.txt" "${f}.txt"
383	done
384}
385tftp_put_multi_host_cleanup() {
386	stop_tftpd
387}
388
389atf_test_case tftp_url_host cleanup
390tftp_url_host_head() {
391	atf_set "descr" "URL with hostname"
392	atf_set "require.user" "root"
393}
394tftp_url_host_body() {
395	start_tftpd
396	local remote_file="${tftp_dir}/hello.txt"
397	echo "Hello, $$!" >"${remote_file}"
398	local local_file="${remote_file##*/}"
399	atf_check -o match:"Received [0-9]+ bytes" \
400	    tftp tftp://localhost/"${remote_file##*/}"
401	atf_check cmp -s "${local_file}" "${remote_file}"
402}
403tftp_url_host_cleanup() {
404	stop_tftpd
405}
406
407atf_test_case tftp_url_ipv4 cleanup
408tftp_url_ipv4_head() {
409	atf_set "descr" "URL with IPv4 address"
410	atf_set "require.user" "root"
411}
412tftp_url_ipv4_body() {
413	start_tftpd
414	local remote_file="${tftp_dir}/hello.txt"
415	echo "Hello, $$!" >"${remote_file}"
416	local local_file="${remote_file##*/}"
417	atf_check -o match:"Received [0-9]+ bytes" \
418	    tftp tftp://127.0.0.1/"${remote_file##*/}"
419	atf_check cmp -s "${local_file}" "${remote_file}"
420}
421tftp_url_ipv4_cleanup() {
422	stop_tftpd
423}
424
425atf_test_case tftp_url_ipv6 cleanup
426tftp_url_ipv6_head() {
427	atf_set "descr" "URL with IPv6 address"
428	atf_set "require.user" "root"
429}
430tftp_url_ipv6_body() {
431	sysctl -q kern.features.inet6 || atf_skip "This test requires IPv6 support"
432	atf_expect_fail "tftp does not support bracketed IPv6 literals in URLs"
433	start_tftpd
434	local remote_file="${tftp_dir}/hello.txt"
435	echo "Hello, $$!" >"${remote_file}"
436	local local_file="${remote_file##*/}"
437	atf_check -o match:"Received [0-9]+ bytes" \
438	    tftp tftp://"[::1]"/"${remote_file##*/}"
439	atf_check cmp -s "${local_file}" "${remote_file}"
440}
441tftp_url_ipv6_cleanup() {
442	stop_tftpd
443}
444
445atf_init_test_cases() {
446	atf_add_test_case tftp_get_big
447	atf_add_test_case tftp_get_host
448	atf_add_test_case tftp_get_ipv4
449	atf_add_test_case tftp_get_ipv6
450	atf_add_test_case tftp_get_one
451	atf_add_test_case tftp_get_two
452	atf_add_test_case tftp_get_more
453	atf_add_test_case tftp_get_multi_host
454	atf_add_test_case tftp_put_big
455	atf_add_test_case tftp_put_host
456	atf_add_test_case tftp_put_ipv4
457	atf_add_test_case tftp_put_ipv6
458	atf_add_test_case tftp_put_one
459	atf_add_test_case tftp_put_two
460	atf_add_test_case tftp_put_more
461	atf_add_test_case tftp_put_multi_host
462	atf_add_test_case tftp_url_host
463	atf_add_test_case tftp_url_ipv4
464	atf_add_test_case tftp_url_ipv6
465}
466