. "$BST_LIB" # BST-TIMEOUT: 600 # ncftp 3.2.7 (meta-networking recipes-daemons/ncftp) -- the NcFTP client suite: # the interactive `ncftp' shell plus ncftpget, ncftpput, ncftpls and the # ncftpbatch/ncftpspooler pair. It is the FIRST FTP CLIENT ON THIS IMAGE THAT # CAN WRITE as a program of its own: tnftp uploads only through `-u', and # ncftpput is a whole binary for it. # # THE SERVER IS THE vsftpd ALREADY ON THIS IMAGE, DRIVEN UNDER inetd. That is # not a convenience, it is the only shape that serves a whole session here: # 89-vsftpd.test establishes that vsftpd's STANDALONE arm accepts, forks, and # the child cannot then move bytes on the socket it inherited # (brintos/hwjs-cc#80), while with listen=NO the session runs entirely in the # process inetd exec'd and no fork happens at all. This suite reuses that # configuration verbatim but for the write settings below, so a red here is # about ncftp rather than about the server -- 89-vsftpd.test is the control that # says the same server serves. # # THE SERVER IS WRITABLE HERE, AND ONLY IN ONE SUBDIRECTORY. 89-vsftpd.test runs # with write_enable=NO because that is vsftpd's own ticket criterion; this one # needs an upload target, so write_enable and anon_upload_enable are on and the # writable place is root/incoming rather than the anon root. That is the standard # anonymous-FTP layout and it also sidesteps vsftpd's refusal to serve a writable # root. anon_mkdir_write_enable and anon_other_write_enable stay off, which gives # the suite its negative control for free. # # THE SERVER DOES NOT CHROOT, SO EVERY REMOTE PATH HERE IS RELATIVE. chroot(2) # is unwired on this platform (brintos/linux#58), so vsftpd's anonymous session # runs in the real filesystem: it chdir()s to anon_root and stops there. # 89-vsftpd.test pins exactly that and is the witness -- its client CWDs to the # HOST path $D/root, gets 250, and vsftpd/pwd asserts PWD answers that same # host path rather than `/'. A remote `/incoming' therefore names the image's # own root, which has no such directory, and the CWD is refused. Naming the # target `incoming', relative to the login directory, is correct whether or not # chroot is ever wired, so it carries no workaround marker: nothing about ncftp # is being worked around. # # NOTHING HERE WAITS ON A SIGNAL, AND NOTHING RUNS AN ncftp IN THE FOREGROUND. # Signal delivery is unreliable (brintos/linux#21), so ncftp's own SIGALRM # transfer timeouts cannot be relied on: a client blocked on a socket the server # accepted and abandoned would never return, and -- because the runner buffers a # suite's report and prints it only when the suite exits -- would cost this file # EVERY case, not the last one. Every client run therefore goes through bg(), # which polls for a completion file and gives up on a count. This is the lesson # 88-tnftp-net.test paid for. # # THE EXIT CODES ARE READ OUT OF THE SOURCE, NOT GUESSED. sh_util/gpshare.h: # kExitSuccess 0, kExitOpenFailed 1, kExitOpenTimedOut 2, kExitXferFailed 3, # kExitXferTimedOut 4, kExitChdirFailed 5, kExitChdirTimedOut 6, # kExitMalformedURL 7, kExitUsage 8, ..., kExitMkdirFailed 13. # A REFUSED LOGIN IS NOT A CODE OF ITS OWN -- it is part of opening the # connection, so it comes out as kExitOpenFailed, 1. # BRINTOS-1149: brintos_local_verify.py loses a suite's FIRST BST result line # before the host parser sees it, and counts the dropped line as a pass. So the # first line out of this file asserts something already true. Same idiom, and # the same reason, as 89-vsftpd and 82-double-conversion. Remove when 1149 is # fixed. bst_ok ncftp/suite-canary require ncftpget ncftp/present GET=$(command -v ncftpget) PUT=$(command -v ncftpput) LS=$(command -v ncftpls) VS=/usr/sbin/vsftpd INETD=/usr/bin/inetd.inetutils PORT=8944 D=/tmp/.bst-ncftp rm -rf "$D"; mkdir -p "$D/root/incoming" "$D/dl" || { bst_notok ncftp/workdir "cannot create $D"; exit 0; } # ncftp writes prefs, a bookmark file and a firewall config under $HOME/.ncftp # on almost every run. Pointing HOME at the work dir keeps a self-test from # editing the image's real home, and keeps a stale bookmark from a previous boot # from deciding what a case below connects to. HOME=$D; export HOME # ~8 KB of fixed text -- large enough that a short transfer is visible, small # enough to stay affordable in a guest that is charged for every byte it holds. # Byte-identical in shape to 89-vsftpd.test's fixture, deliberately: the two # suites move the same file through the same server by different clients. awk 'BEGIN{for(n=1;n<=140;n++) printf "%04d the quick brown fox jumps over the lazy dog 0123456789\n", n}' > "$D/root/payload" awk 'BEGIN{for(n=1;n<=90;n++) printf "%04d upload line for the ncftpput leg of this suite\n", n}' > "$D/upme" chmod 644 "$D/root/payload" "$D/upme" chmod 755 "$D" "$D/root" "$D/dl" chmod 777 "$D/root/incoming" FIXSZ=$(wc -c < "$D/root/payload" | tr -d ' ') UPSZ=$(wc -c < "$D/upme" | tr -d ' ') # bg -- run a client that MAY NEVER RETURN in the # background and poll up to for it, without wait(1). The rc travels out # through a file because $? is not available to a poll. Sets bg_done=1/0, and # bg_rc/bg_out when bg_done=1. Lifted from 88-tnftp-net.test for the reason # given in the header. bg() { _t=$1; _tag=$2; shift 2 rm -f "$D/$_tag.rc" "$D/$_tag.out" ( "$@" > "$D/$_tag.out" 2>&1; echo $? > "$D/$_tag.rc" ) & _i=0; bg_done=0 while [ "$_i" -lt "$_t" ]; do if [ -f "$D/$_tag.rc" ]; then bg_done=1; break; fi _i=$((_i+1)); sleep 1 done if [ "$bg_done" -eq 1 ]; then bg_rc=$(cat "$D/$_tag.rc" 2>/dev/null); bg_out=$(cat "$D/$_tag.out" 2>/dev/null) else bg_rc=""; bg_out=$(cat "$D/$_tag.out" 2>/dev/null) fi } # want_rc -- the bounded form of check_rc. A # client that never returned is a red naming the bound, not a wrong status: # those two failures have different causes and the detail has to say which. want_rc() { _id=$1; _w=$2; shift 2 bg "$@" if [ "$bg_done" -eq 0 ]; then bst_notok "$_id" "never returned within the bound; got:'$(_bst_1line "$bg_out")'" elif [ "$bg_rc" = "$_w" ]; then bst_ok "$_id" elif bst_execfail "$bg_out"; then bst_unrun "$_id" "$bg_out" else bst_notok "$_id" "rc=$bg_rc want=$_w got:'$(_bst_1line "$bg_out")'"; fi } # has has() { case "$3" in *"$2"*) bst_ok "$1" ;; *) bst_notok "$1" "want:'$2' got:'$(_bst_1line "$3")'" ;; esac } ## ## 1. Identity and the packaging contract ## # All five programs are one package and one do_install; a missing one is a # packaging regression rather than a build failure, and would otherwise surface # further down as a skip that reads like an absent port. for p in ncftp ncftpget ncftpput ncftpls ncftpbatch; do if command -v "$p" >/dev/null 2>&1; then bst_ok "ncftp/have-$p" else bst_notok "ncftp/have-$p" "$p is not on the PATH, so the package installed less than it builds"; fi done # Usage() exits kExitUsage and prints the version banner on the way out. Two # cases rather than one: the status and the banner come from different places # (the exit at the end of Usage, and the fprintf at its head through OpenPager), # and a pager that swallowed the text would otherwise take the status with it. want_rc ncftp/usage-rc 8 20 usage "$LS" has ncftp/version "3.2.7" "$bg_out" ## ## 2. The session, against vsftpd under inetd ## skip_net() { for c in inetd-registered get-rc get-bytewise get-stdout-len ls-rc ls-names \ put-rc put-bytewise put-sendfile-arm badlogin-rc missing-file-rc \ mkdir-refused-rc; do bst_skip "ncftp/$c" "$1" done } if [ ! -x "$VS" ]; then skip_net "vsftpd is not installed, and it is the server this suite drives" elif [ ! -x "$INETD" ]; then skip_net "inetutils' inetd is not installed, and vsftpd only serves a whole session under it" else cat > "$D/vsftpd.conf" < "$D/inetd.tab" "$INETD" -d "$D/inetd.tab" > "$D/inetd.log" 2>&1 & i=0 while [ $i -lt 40 ]; do grep -q 'registered ' "$D/inetd.log" 2>/dev/null && break i=$((i+1)); sleep 1 done if grep -q 'registered ' "$D/inetd.log" 2>/dev/null; then bst_ok ncftp/inetd-registered else bst_notok ncftp/inetd-registered "inetd bound no service after $i polls: $(_bst_1line "$(cat "$D/inetd.log" 2>/dev/null)")" fi # --- download ------------------------------------------------------------- # THE TICKET'S FIRST NAMED CRITERION, in two cases deliberately: a transfer # that "succeeds" having moved the wrong bytes is the failure a status check # cannot see. want_rc ncftp/get-rc 0 90 get "$GET" -P "$PORT" 127.0.0.1 "$D/dl" payload if cmp -s "$D/root/payload" "$D/dl/payload" 2>/dev/null; then bst_ok ncftp/get-bytewise else bst_notok ncftp/get-bytewise "the retrieved copy differs from the source ($(wc -c < "$D/dl/payload" 2>/dev/null | tr -d ' ') of $FIXSZ bytes on disk)"; fi # -c writes the body to stdout instead of to a file, which is a different # sink inside ncftpget and not merely a different filename. bg 90 getc "$GET" -c -P "$PORT" 127.0.0.1 payload if [ "$bg_done" -eq 1 ] && [ "$bg_rc" = 0 ]; then n=$(wc -c < "$D/getc.out" | tr -d ' ') if [ "$n" = "$FIXSZ" ]; then bst_ok ncftp/get-stdout-len else bst_notok ncftp/get-stdout-len "-c wrote $n bytes of $FIXSZ"; fi else bst_notok ncftp/get-stdout-len "rc=${bg_rc:-} want=0: $(_bst_1line "$bg_out")" fi # --- listing -------------------------------------------------------------- # -1 is the one-item-per-line format, so the assertion is on names rather than # on a column layout that says nothing about the protocol. want_rc ncftp/ls-rc 0 90 lsrun "$LS" -P "$PORT" -1 "ftp://127.0.0.1/" ls_out=$bg_out if printf '%s\n' "$ls_out" | grep -q '^payload$' \ && printf '%s\n' "$ls_out" | grep -q '^incoming'; then bst_ok ncftp/ls-names else bst_notok ncftp/ls-names "the listing names neither both of payload and incoming: '$(_bst_1line "$ls_out")'" fi # --- upload --------------------------------------------------------------- # THE TICKET'S SECOND NAMED CRITERION. The comparison is against the file the # SERVER now holds, read back off its own directory, not against what we sent. want_rc ncftp/put-rc 0 90 put "$PUT" -P "$PORT" 127.0.0.1 incoming "$D/upme" if cmp -s "$D/upme" "$D/root/incoming/upme" 2>/dev/null; then bst_ok ncftp/put-bytewise else bst_notok ncftp/put-bytewise "the uploaded copy differs from the source ($(wc -c < "$D/root/incoming/upme" 2>/dev/null | tr -d ' ') of $UPSZ bytes landed)"; fi # THE PLATFORM GAP, MEASURED RATHER THAN HIDDEN. The recipe patches ncftpput's # compiled-in default to the userspace copy loop because sendfile(2) is ENOSYS # here (brintos/linux-in-the-browser#151) and io_put.c has no fallback once it # has chosen the sendfile branch. -s is a TOGGLE, so against the patched build # it selects sendfile -- which is what makes this case a live measurement of # the gap rather than a comment about it. It is expected NON-ZERO today and # will start failing, correctly, the day #151 is wired: at that point the # patch and this case go together. # BRINTOS-WORKAROUND: brintos/linux-in-the-browser#151 remove -- when sendfile # is wired, drop the recipe patch and assert rc 0 here instead. bg 90 puts "$PUT" -s -P "$PORT" 127.0.0.1 incoming "$D/upme" rm -f "$D/root/incoming/upme" if [ "$bg_done" -eq 1 ] && [ "$bg_rc" != 0 ]; then bst_ok ncftp/put-sendfile-arm elif [ "$bg_done" -eq 1 ]; then bst_notok ncftp/put-sendfile-arm "ncftpput -s (the sendfile arm) SUCCEEDED, so sendfile(2) now works and linux-in-the-browser#151 has closed -- drop the recipe patch and make this case assert rc 0" else bst_notok ncftp/put-sendfile-arm "the sendfile arm never returned within the bound, so #151's failure mode has changed from an error to a hang" fi # --- the negative controls ------------------------------------------------ # THE TICKET'S THIRD NAMED CRITERION. This server is anonymous-only, so a # WRONG PASSWORD against `anonymous' is accepted by definition -- an anonymous # login takes any password. The refusal that can be measured is therefore a # credential the server will not take at all, and it exercises exactly the # code path the criterion is about: ncftpget reads the server's 530 while # opening the connection and exits kExitOpenFailed. want_rc ncftp/badlogin-rc 1 60 bad \ "$GET" -P "$PORT" -u bstnosuchuser -p wrong.password 127.0.0.1 "$D/dl" payload # A file the server does not have. kExitXferFailed, 3 -- the connection opened # and the transfer is what failed, which is the distinction the exit-code # table exists to draw. want_rc ncftp/missing-file-rc 3 60 miss \ "$GET" -P "$PORT" 127.0.0.1 "$D/dl" no-such-file-here # anon_mkdir_write_enable=NO is asserted through ncftpput's -m, which mkdirs # the destination before copying. Without it, put-rc above would pass equally # against a server that had stopped enforcing anything. # Either exit code is the refusal: ncftpput -m tries the full path, then one # component at a time, and MKD nested is answered 550 in both arms -- which # comes out as kExitMkdirFailed 13 from the full-path arm and kExitChdirFailed # 5 from the one-at-a-time arm, depending on which of the two ran last. The # case is about the server refusing, not about which arm reported it. bg 60 mkd "$PUT" -m -P "$PORT" 127.0.0.1 incoming/nested "$D/upme" if [ "$bg_done" -eq 0 ]; then bst_notok ncftp/mkdir-refused-rc "never returned within the bound; got:'$(_bst_1line "$bg_out")'" else case "$bg_rc" in 5|13) bst_ok ncftp/mkdir-refused-rc ;; *) bst_notok ncftp/mkdir-refused-rc "rc=$bg_rc want 5 (kExitChdirFailed) or 13 (kExitMkdirFailed): $(_bst_1line "$bg_out")" ;; esac fi fi rm -rf "$D"