diff --git a/apps/wolfssh/wolfssh.c b/apps/wolfssh/wolfssh.c index 8a5a558f..9cd153ab 100644 --- a/apps/wolfssh/wolfssh.c +++ b/apps/wolfssh/wolfssh.c @@ -338,7 +338,7 @@ static int FlushQueuedSend(WOLFSSH* ssh, wolfSSL_Mutex* lock) * conversation is for the reader to sort out. A rekey started on the way * through is the reader's as well, the send itself went out. */ if (ret == WS_WANT_READ || ret == WS_CHAN_RXD || ret == WS_EXTDATA - || ret == WS_REKEYING) { + || ret == WS_REKEYING || ret == WS_EOF) { ret = WS_SUCCESS; } @@ -1362,14 +1362,17 @@ static THREAD_RETURN WOLFSSH_THREAD wolfSSH_Client(void* args) ret = WS_SUCCESS; } } - else if (ret != WS_CHANNEL_CLOSED && ret != WS_WANT_READ) { + else if (ret != WS_CHANNEL_CLOSED && ret != WS_WANT_READ + && ret != WS_EOF) { WLOG(WS_LOG_DEBUG, "Sending the shutdown messages failed."); } - if (ret == WS_CHANNEL_CLOSED || ret == WS_WANT_READ) { - /* Shutting down. The channel closing isn't a fail, and neither - * is the peer having nothing ready on this non-blocking socket; - * either way there is nothing left to wait for. */ + if (ret == WS_CHANNEL_CLOSED || ret == WS_WANT_READ + || ret == WS_EOF) { + /* Shutting down. The channel closing or the peer's EOF isn't a + * fail, and neither is the peer having nothing ready on this + * non-blocking socket; either way there is nothing left to wait + * for. */ ret = WS_SUCCESS; } else if (ret != WS_SUCCESS) { diff --git a/apps/wolfsshd/test/run_all_sshd_tests.sh b/apps/wolfsshd/test/run_all_sshd_tests.sh index d20acb40..44042cc9 100755 --- a/apps/wolfsshd/test/run_all_sshd_tests.sh +++ b/apps/wolfsshd/test/run_all_sshd_tests.sh @@ -11,6 +11,7 @@ test_cases=( "sshd_sftp_idle_cpu_test.sh" "sshd_scp_fail.sh" "sshd_term_close_test.sh" + "sshd_stdin_eof_test.sh" "ssh_kex_algos.sh" ) diff --git a/apps/wolfsshd/test/sshd_stdin_eof_test.sh b/apps/wolfsshd/test/sshd_stdin_eof_test.sh new file mode 100755 index 00000000..65132432 --- /dev/null +++ b/apps/wolfsshd/test/sshd_stdin_eof_test.sh @@ -0,0 +1,132 @@ +#!/bin/bash +# bash, unlike the rest of this directory: the option array and PIPESTATUS +# below both need it. + +# A client that half-closes its stdin sends SSH_MSG_CHANNEL_EOF and waits for +# the command to finish. wolfSSHd must close the write end of the child's stdin +# pipe so a command reading to end-of-input returns, and it must hand over +# everything the peer sent before it does. +# +# Needs the OpenSSH client; the wolfSSH example client does not half-close. + +if [ -z "$1" ] || [ -z "$2" ]; then + echo "expecting host and port as arguments" + echo "./sshd_stdin_eof_test.sh 127.0.0.1 22222" + exit 1 +fi + +HOST="$1" +PORT="$2" +USER_NAME="${3:-`whoami`}" + +command -v ssh >/dev/null 2>&1 || { + echo "ssh not found, skipping" + exit 77 +} + +# The RESULT==124 assertions below are the point of the test. +command -v timeout >/dev/null 2>&1 || { + echo "timeout not found, skipping" + exit 77 +} + +# ssh refuses a group/world readable identity file. +KEYDIR=`mktemp -d 2>/dev/null` || KEYDIR=`mktemp -d -t sshdeof` +if [ -z "$KEYDIR" ] || [ ! -d "$KEYDIR" ]; then + echo "could not create temp dir" + exit 1 +fi +trap 'rm -rf "$KEYDIR"' EXIT + +cp ../../../keys/hansel-key-ecc.pem "$KEYDIR/id_ecdsa" || exit 1 +chmod 600 "$KEYDIR/id_ecdsa" + +SSH_OPTS=(-i "$KEYDIR/id_ecdsa" -p "$PORT" + -o IdentitiesOnly=yes + -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null + -o PreferredAuthentications=publickey -o PasswordAuthentication=no + -o BatchMode=yes -o ConnectTimeout=5 -o LogLevel=ERROR) + +# An identity the client cannot load, or a host it cannot reach, is not this +# test's subject. sshd_exec_test.sh runs ahead of this one and owns a server +# that cannot run commands at all. +if ! timeout 20 ssh "${SSH_OPTS[@]}" "$USER_NAME@$HOST" true >/dev/null 2>&1 +then + echo "no session with these options, skipping" + exit 77 +fi + +# Case 1: a small input through 'sort'. 'sort' emits nothing until +# end-of-input, so a missed EOF is the timeout and a killed or hung child is +# empty output -- neither can pass by winning a race the way a streaming 'cat' +# can. The input is unsorted so the comparison also proves the remote command +# ran rather than the input being echoed back. +printf 'charlie\nalpha\nbravo\n' > "$KEYDIR/in.txt" + +# wolfsshd runs the user's login shell, so its startup files can print on +# either stream: stderr goes to a file, and stdout is filtered to the lines +# the command itself produced. +OUT=`timeout 20 ssh "${SSH_OPTS[@]}" "$USER_NAME@$HOST" 'sort' \ + < "$KEYDIR/in.txt" 2> "$KEYDIR/in.err" \ + | grep -E '^(alpha|bravo|charlie)$'` +RESULT=${PIPESTATUS[0]} + +if [ "$RESULT" == 124 ]; then + echo "session did not end after the client half-closed its stdin" + cat "$KEYDIR/in.err" + exit 1 +fi + +if [ "$RESULT" != 0 ]; then + echo "ssh failed with $RESULT" + cat "$KEYDIR/in.err" + exit 1 +fi + +if [ "$OUT" != "`sort "$KEYDIR/in.txt"`" ]; then + echo "unexpected output from the remote command" + echo "$OUT" + cat "$KEYDIR/in.err" + exit 1 +fi + +# Case 2: the same half-close with the send window full. The reader below +# stalls, so the client stops draining, the server's window to the peer fills +# while the client is still sending, and the EOF arrives with channel data +# still buffered on the server. Data held back that way has to be handed to +# the child before its stdin closes: dropping it truncates the output, and +# never handing it over leaves 'cat' waiting on a stdin that never closes. +awk 'BEGIN { for (i = 0; i < 200000; i++) + printf "%08d one two three four five six seven\n", (i * 48271) % 99991 + }' > "$KEYDIR/big.txt" + +timeout 90 ssh "${SSH_OPTS[@]}" "$USER_NAME@$HOST" 'cat' \ + < "$KEYDIR/big.txt" 2> "$KEYDIR/big.err" \ + | { sleep 5; cat; } > "$KEYDIR/big.out" +RESULT=${PIPESTATUS[0]} + +if [ "$RESULT" == 124 ]; then + echo "session did not end after a half-close with the window full" + cat "$KEYDIR/big.err" + exit 1 +fi + +if [ "$RESULT" != 0 ]; then + echo "ssh failed with $RESULT" + cat "$KEYDIR/big.err" + exit 1 +fi + +SENT=`wc -c < "$KEYDIR/big.txt"` +GOT=`wc -c < "$KEYDIR/big.out"` + +if [ "$GOT" -lt "$SENT" ] \ + || ! tail -c "$SENT" "$KEYDIR/big.out" | cmp -s - "$KEYDIR/big.txt" +then + echo "the remote command did not see all of the input" + echo "sent $SENT bytes, got $GOT bytes" + cat "$KEYDIR/big.err" + exit 1 +fi + +exit 0 diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 3a7c4cdd..08043954 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -908,6 +908,8 @@ static int SFTP_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, * if there is still pending sends */ } if (error == WS_EOF) { + /* An ordinary session end, not a failure. */ + ret = 0; break; } } @@ -934,10 +936,19 @@ static int SFTP_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, continue; } + /* Drain what is buffered first. A rekey is not a drained + * channel: peek reports it without looking. */ if (error == WS_EOF) { - break; + int peekRet = wolfSSH_stream_peek(ssh, NULL, 1); + + if (peekRet != WS_REKEYING && peekRet <= 0) { + /* An ordinary session end, not a failure. */ + ret = 0; + break; + } } - if (ret != WS_SUCCESS && ret != WS_CHAN_RXD) { + if (ret != WS_SUCCESS && ret != WS_CHAN_RXD + && ret != WS_EOF) { /* If not successful and no channel data, leave. */ break; } @@ -954,8 +965,11 @@ static int SFTP_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, error == WS_CHAN_RXD || error == WS_REKEYING || error == WS_WINDOW_FULL) ret = error; - if (error == WS_EOF) + if (error == WS_EOF) { + /* An ordinary session end, not a failure. */ + ret = 0; break; + } continue; } else if (ret == WS_REKEYING) { @@ -964,8 +978,11 @@ static int SFTP_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, } else if (ret < 0) { error = wolfSSH_get_error(ssh); - if (error == WS_EOF) + if (error == WS_EOF) { + /* An ordinary session end, not a failure. */ + ret = 0; break; + } } else { /* Channel is live with nothing buffered. Let the next select @@ -1331,6 +1348,19 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, else if (rc == WS_CHANNEL_CLOSED) { continue; } + else if (rc == WS_EOF) { + /* The peer is done sending. No EOF of ours here: it + * latches eofTxd and the child's remaining console + * output would then be refused, which both send sites + * below treat as fatal. wolfSSH_shutdown() sends it at + * teardown, as the POSIX copy relies on. Closing the + * write end of the child's stdin is still owed on this + * platform, and so is the per-pass drain: ptyIn is the + * terminal-resize context, and this copy reads into + * shellBuffer, which the windowFull resend owes the + * peer. Both want fixing where they can be tested. */ + continue; + } else if (rc != WS_WANT_READ) { break; } @@ -1524,8 +1554,10 @@ static int SHELL_FlushOut(WOLFSSH* ssh, WS_SOCKET_T sshFd, word32 channelId, if (wolfSSH_worker(ssh, NULL) < 0) { int err = wolfSSH_get_error(ssh); + /* A peer EOF during the final flush is expected. */ if (err != WS_WANT_READ && err != WS_WANT_WRITE && - err != WS_CHAN_RXD && err != WS_REKEYING) { + err != WS_CHAN_RXD && err != WS_REKEYING && + err != WS_EOF) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Issue draining connection on final flush"); return -1; @@ -1830,11 +1862,9 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, word32 shellChannelId = 0; WOLFSSH_CHANNEL* shellChannel; - /* Name the session channel off the channel list rather than trusting - * DEFAULT_NEXT_CHANNEL to be 0, which a build can override. It is the - * only channel open at this point; the agent channel comes later. The - * loop below closes the child's stdin off this channel, so a wrong id - * there drops the peer's input instead of handing it over. */ + /* Off the channel list, not DEFAULT_NEXT_CHANNEL, which a build can + * override. The session channel is the only one open here. lastRxId is + * no use: no request path sets it. A wrong id drops the peer's input. */ shellChannel = wolfSSH_ChannelNext(ssh, NULL); if (shellChannel == NULL || wolfSSH_ChannelGetId(shellChannel, &shellChannelId, WS_CHANNEL_ID_SELF) != WS_SUCCESS) { @@ -1966,6 +1996,9 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, peerConnected = 0; continue; } + else if (rc == WS_EOF) { + /* Half-close, handled below. */ + } else if (rc == WS_WANT_WRITE) { wantWrite = 1; continue; @@ -2722,7 +2755,7 @@ static void* HandleConnection(void* arg) error = wolfSSH_get_error(ssh); /* peer successfully closed down gracefully */ - if (ret == WS_CHANNEL_CLOSED) { + if (ret == WS_CHANNEL_CLOSED || ret == WS_EOF) { ret = 0; break; } diff --git a/examples/client/client.c b/examples/client/client.c index b397075e..7abdffe6 100644 --- a/examples/client/client.c +++ b/examples/client/client.c @@ -1179,7 +1179,7 @@ THREAD_RETURN WOLFSSH_THREAD client_test(void* args) if (ret <= 0) { ret = wolfSSH_get_error(ssh); if (ret != WS_WANT_READ && ret != WS_WANT_WRITE && - ret != WS_CHAN_RXD) { + ret != WS_CHAN_RXD && ret != WS_EOF) { ClientFreeBuffers(pubKeyName, privKeyName, NULL); wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); @@ -1198,7 +1198,9 @@ THREAD_RETURN WOLFSSH_THREAD client_test(void* args) #endif } ret = wolfSSH_shutdown(ssh); - /* do not continue on with shutdown process if peer already disconnected */ + /* do not continue on with shutdown process if peer already disconnected. + * A peer EOF is not a disconnect: the channel is still open and its close + * is still owed, so the drain below is exactly what is wanted. */ if (ret != WS_SOCKET_ERROR_E && wolfSSH_get_error(ssh) != WS_SOCKET_ERROR_E && wolfSSH_get_error(ssh) != WS_CHANNEL_CLOSED) { if (ret != WS_SUCCESS) { @@ -1209,7 +1211,7 @@ THREAD_RETURN WOLFSSH_THREAD client_test(void* args) } ret = wolfSSH_worker(ssh, NULL); if (ret != WS_SUCCESS && ret != WS_SOCKET_ERROR_E && - ret != WS_CHANNEL_CLOSED) { + ret != WS_CHANNEL_CLOSED && ret != WS_EOF) { ClientFreeBuffers(pubKeyName, privKeyName, NULL); wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); @@ -1226,7 +1228,7 @@ THREAD_RETURN WOLFSSH_THREAD client_test(void* args) wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); if (ret != WS_SUCCESS && ret != WS_SOCKET_ERROR_E && - ret != WS_CHANNEL_CLOSED) { + ret != WS_CHANNEL_CLOSED && ret != WS_EOF) { err_sys("Closing client stream failed"); } diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 35094b4f..b5f86c3e 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -805,6 +805,10 @@ static int ssh_worker(thread_ctx_t* threadCtx) WOLFSSH* ssh; WS_SOCKET_T sshFd; int rc = 0; + int eofAnswered = 0; + /* Without a shell there is no child to outlive the peer's EOF, and the + * read path echoes unconditionally. */ + int echoOnly = 1; #ifdef WOLFSSH_SHELL const char *userName; struct passwd *p_passwd; @@ -822,6 +826,10 @@ static int ssh_worker(thread_ctx_t* threadCtx) if (ssh == NULL) return WS_FATAL_ERROR; +#ifdef WOLFSSH_SHELL + echoOnly = threadCtx->echo; +#endif + sshFd = wolfSSH_get_fd(ssh); #if defined(WOLFSSL_PTHREADS) && defined(WOLFSSL_TEST_GLOBAL_REQ) @@ -997,6 +1005,57 @@ static int ssh_worker(thread_ctx_t* threadCtx) channel. The additional channel is only used with the agent. */ cnt_r = wolfSSH_worker(ssh, &lastChannel); + + /* The peer is done sending: hand back the backlog and answer + * its EOF, or a client that half-closed waits on a server + * that never finishes -- the library no longer answers for + * us. Off the channel's own state, not the WS_EOF status: the + * flush inside wolfSSH_worker() can supersede that, and it is + * raised once. Echo mode only; a shell child on a pty is + * still producing, so its EOF waits for the child to exit. */ + if (!eofAnswered && echoOnly) { + WOLFSSH_CHANNEL* eofChannel; + + eofChannel = wolfSSH_ChannelFind(ssh, + threadCtx->shellCtx.channelId, WS_CHANNEL_ID_SELF); + if (eofChannel != NULL + && wolfSSH_ChannelGetEof(eofChannel)) { + int eofRead; + int eofSent; + int eofOff; + + do { + eofRead = wolfSSH_ChannelIdRead(ssh, + threadCtx->shellCtx.channelId, + threadCtx->channelBuffer, + sizeof threadCtx->channelBuffer); + eofOff = 0; + /* A send is bounded by the peer's window and + * packet size, so a short one is normal and the + * rest of the chunk is still owed. */ + while (eofOff < eofRead) { + eofSent = wolfSSH_ChannelIdSend(ssh, + threadCtx->shellCtx.channelId, + threadCtx->channelBuffer + eofOff, + eofRead - eofOff); + if (eofSent <= 0) + break; + eofOff += eofSent; + } + if (eofOff < eofRead) + break; + } while (eofRead > 0); + + /* Only an emptied channel earns the EOF; anything + * else is retried on a later pass. */ + if (eofRead == 0) { + wolfSSH_ChannelSendEof(eofChannel); + eofAnswered = 1; + ChildRunning = 0; + } + } + } + if (cnt_r < 0) { rc = wolfSSH_get_error(ssh); /* wolfSSH_worker() reports WS_REKEYING in place of @@ -1117,6 +1176,11 @@ static int ssh_worker(thread_ctx_t* threadCtx) #endif continue; } + else if (rc == WS_EOF) { + /* The half-close is answered by the durable check + * above, which has already run this pass. */ + continue; + } else if (rc != WS_WANT_READ) { #ifdef SHELL_DEBUG printf("Break:read sshFd returns %d: errno =%x\n", @@ -1439,6 +1503,8 @@ static int sftp_worker(thread_ctx_t* threadCtx) * if there is still pending sends */ } if (error == WS_EOF) { + /* An ordinary session end, not a failure. */ + ret = 0; break; } } @@ -1465,10 +1531,18 @@ static int sftp_worker(thread_ctx_t* threadCtx) ret = error; } + /* Drain what is buffered before leaving on the EOF. */ if (error == WS_EOF) { - break; + /* A rekey is not a drained channel. */ + int peekRet = wolfSSH_stream_peek(ssh, NULL, 1); + + if (peekRet != WS_REKEYING && peekRet <= 0) { + /* An ordinary session end, not a failure. */ + ret = 0; + break; + } } - if (ret != WS_SUCCESS && ret != WS_CHAN_RXD) { + if (ret != WS_SUCCESS && ret != WS_CHAN_RXD && ret != WS_EOF) { #ifdef WOLFSSH_TEST_BLOCK if (error == WS_WANT_READ) { while (error == WS_WANT_READ) { @@ -1504,8 +1578,10 @@ static int sftp_worker(thread_ctx_t* threadCtx) error == WS_CHAN_RXD || error == WS_REKEYING || error == WS_WINDOW_FULL) ret = error; - if (error == WS_EOF) + if (error == WS_EOF) { + ret = 0; break; + } continue; } else if (ret == WS_REKEYING) { @@ -1675,6 +1751,13 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs) ret = 0; } + /* The peer's close already retired the channel: a completed + * shutdown, not a failure. Left non-zero it sets quit, taking the + * server down after one session. */ + if (ret == WS_CHANNEL_CLOSED) { + ret = 0; + } + error = wolfSSH_get_error(threadCtx->ssh); if (error != WS_SOCKET_ERROR_E && (error == WS_WANT_READ || error == WS_WANT_WRITE)) { @@ -1686,7 +1769,7 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs) error = wolfSSH_get_error(threadCtx->ssh); /* peer successfully closed down gracefully */ - if (ret == WS_CHANNEL_CLOSED) { + if (ret == WS_CHANNEL_CLOSED || ret == WS_EOF) { ret = 0; break; } diff --git a/examples/portfwd/portfwd.c b/examples/portfwd/portfwd.c index 7e26a969..e562ac4c 100644 --- a/examples/portfwd/portfwd.c +++ b/examples/portfwd/portfwd.c @@ -431,6 +431,7 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) int ret; int ch; int appFdSet = 0; + int appFdHalfClosed = 0; int reverse = 0; int fwdFromPortSet = 0; PortfwdState fwdState; @@ -641,7 +642,8 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) ret = wolfSSH_worker(ssh, NULL); if (ret != WS_SUCCESS && ret != WS_CHAN_RXD && ret != WS_WANT_READ && ret != WS_WANT_WRITE && - ret != WS_WINDOW_FULL && ret != WS_REKEYING) + ret != WS_WINDOW_FULL && ret != WS_REKEYING && + ret != WS_EOF) err_sys("Couldn't get the remote forward reply."); } if (!fwdState.replied) @@ -765,6 +767,44 @@ THREAD_RETURN WOLFSSH_THREAD portfwd_worker(void* args) break; } + /* Relay the half-close so a local reader waiting on end-of-input + * returns; nothing else relays it. Driven off the latched channel + * state, not the WS_EOF status: the flush inside wolfSSH_worker() + * can supersede that, and it is raised only once. Only the channel + * appFd is wired to, since half-closing the wrong socket truncates + * a live transfer. */ + if (appFdSet && fwdChannel != NULL && !appFdHalfClosed + && wolfSSH_ChannelGetEof(fwdChannel)) { + int drained; + + /* Hand over the backlog first, or the local reader sees a + * clean end-of-input short of what the peer sent. A negative + * read is a rekey or a stalled channel, not a drained one, so + * only an empty read earns the half-close; the latch stays + * clear and a later pass tries again. */ + do { + drained = wolfSSH_ChannelRead(fwdChannel, sshBuffer, + sshBufferSz); + if (drained > 0) { + if ((int)send(appFd, sshBuffer, drained, 0) != drained) + break; + } + } while (drained > 0); + + if (drained == 0) { + appFdHalfClosed = 1; + #ifdef SHUT_WR + shutdown(appFd, SHUT_WR); + #elif defined(SD_SEND) + shutdown(appFd, SD_SEND); + #else + printf("No way to half-close the local socket, " + "the local reader may wait for input that is " + "not coming.\n"); + #endif + } + } + if (ret == WS_CHAN_RXD) { WOLFSSH_CHANNEL* readChannel; diff --git a/examples/scpclient/scpclient.c b/examples/scpclient/scpclient.c index 8b817ad5..aaa2d7fd 100644 --- a/examples/scpclient/scpclient.c +++ b/examples/scpclient/scpclient.c @@ -317,7 +317,9 @@ THREAD_RETURN WOLFSSH_THREAD scp_client(void* args) } ret = wolfSSH_shutdown(ssh); - /* do not continue on with shutdown process if peer already disconnected */ + /* do not continue on with shutdown process if peer already disconnected. + * A peer EOF is not a disconnect: the channel is still open and its close + * is still owed, so the drain below is exactly what is wanted. */ if (ret != WS_CHANNEL_CLOSED && ret != WS_SOCKET_ERROR_E && wolfSSH_get_error(ssh) != WS_SOCKET_ERROR_E && wolfSSH_get_error(ssh) != WS_CHANNEL_CLOSED) { @@ -326,7 +328,8 @@ THREAD_RETURN WOLFSSH_THREAD scp_client(void* args) } else { ret = wolfSSH_worker(ssh, NULL); - if (ret != WS_SUCCESS && ret != WS_CHANNEL_CLOSED) { + if (ret != WS_SUCCESS && ret != WS_CHANNEL_CLOSED + && ret != WS_EOF) { WLOG(WS_LOG_DEBUG, "Failed to listen for close messages from the peer."); } @@ -336,7 +339,7 @@ THREAD_RETURN WOLFSSH_THREAD scp_client(void* args) wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); if (ret != WS_SUCCESS && ret != WS_SOCKET_ERROR_E && - ret != WS_CHANNEL_CLOSED) { + ret != WS_CHANNEL_CLOSED && ret != WS_EOF) { WLOG(WS_LOG_DEBUG, "Closing scp stream failed. Connection could have been closed by peer"); } @@ -346,7 +349,8 @@ THREAD_RETURN WOLFSSH_THREAD scp_client(void* args) wc_ecc_fp_free(); /* free per thread cache */ #endif - if ((ret != WS_SUCCESS) && (ret != WS_CHANNEL_CLOSED)) + if ((ret != WS_SUCCESS) && (ret != WS_CHANNEL_CLOSED) + && (ret != WS_EOF)) ((func_args*)args)->return_code = 1; return 0; } diff --git a/examples/sftpclient/sftpclient.c b/examples/sftpclient/sftpclient.c index f78c3306..40841f83 100644 --- a/examples/sftpclient/sftpclient.c +++ b/examples/sftpclient/sftpclient.c @@ -1501,7 +1501,7 @@ static int doAutopilot(int cmd, char* local, char* remote) /* wolfSSH_worker returns WS_FATAL_ERROR when the socket * would block (DoReceive -> GetInputData -> WS_WANT_READ), * so check ssh->error rather than ret for blocking conditions. */ - if (ret != WS_SUCCESS && ret != WS_CHAN_RXD && + if (ret != WS_SUCCESS && ret != WS_CHAN_RXD && ret != WS_EOF && wolfSSH_get_error(ssh) != WS_WANT_READ && wolfSSH_get_error(ssh) != WS_WANT_WRITE) break; @@ -1815,7 +1815,9 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) int err; ret = wolfSSH_shutdown(ssh); - /* peer hung up or channel already closed, stop trying */ + /* peer hung up or channel already closed, stop trying. + * wolfSSH_shutdown() folds a peer EOF into WS_SUCCESS itself, so + * there is no WS_EOF to test for here. */ if (ret == WS_SOCKET_ERROR_E || ret == WS_ERROR || ret == WS_CHANNEL_CLOSED) { ret = 0; @@ -1832,7 +1834,7 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) err = wolfSSH_get_error(ssh); /* peer successfully closed down gracefully */ - if (ret == WS_CHANNEL_CLOSED) { + if (ret == WS_CHANNEL_CLOSED || ret == WS_EOF) { ret = 0; break; } diff --git a/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c b/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c index b84c69c6..a42276dd 100644 --- a/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c +++ b/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c @@ -793,6 +793,10 @@ static int ssh_worker(thread_ctx_t* threadCtx) WOLFSSH* ssh; WS_SOCKET_T sshFd; int rc = 0; + int eofAnswered = 0; + /* Without a shell there is no child to outlive the peer's EOF, and the + * read path echoes unconditionally. */ + int echoOnly = 1; #ifdef WOLFSSH_SHELL const char *userName; struct passwd *p_passwd; @@ -810,6 +814,10 @@ static int ssh_worker(thread_ctx_t* threadCtx) if (ssh == NULL) return WS_FATAL_ERROR; +#ifdef WOLFSSH_SHELL + echoOnly = threadCtx->echo; +#endif + sshFd = wolfSSH_get_fd(ssh); #if defined(WOLFSSL_PTHREADS) && defined(WOLFSSL_TEST_GLOBAL_REQ) @@ -981,6 +989,57 @@ static int ssh_worker(thread_ctx_t* threadCtx) channel. The additional channel is only used with the agent. */ cnt_r = wolfSSH_worker(ssh, &lastChannel); + + /* The peer is done sending: hand back the backlog and answer + * its EOF, or a client that half-closed waits on a server + * that never finishes -- the library no longer answers for + * us. Off the channel's own state, not the WS_EOF status: the + * flush inside wolfSSH_worker() can supersede that, and it is + * raised once. Echo mode only; a shell child on a pty is + * still producing, so its EOF waits for the child to exit. */ + if (!eofAnswered && echoOnly) { + WOLFSSH_CHANNEL* eofChannel; + + eofChannel = wolfSSH_ChannelFind(ssh, shellChannelId, + WS_CHANNEL_ID_SELF); + if (eofChannel != NULL + && wolfSSH_ChannelGetEof(eofChannel)) { + int eofRead; + int eofSent; + int eofOff; + + do { + eofRead = wolfSSH_ChannelIdRead(ssh, + shellChannelId, + threadCtx->channelBuffer, + sizeof threadCtx->channelBuffer); + eofOff = 0; + /* A send is bounded by the peer's window and + * packet size, so a short one is normal and the + * rest of the chunk is still owed. */ + while (eofOff < eofRead) { + eofSent = wolfSSH_ChannelIdSend(ssh, + shellChannelId, + threadCtx->channelBuffer + eofOff, + eofRead - eofOff); + if (eofSent <= 0) + break; + eofOff += eofSent; + } + if (eofOff < eofRead) + break; + } while (eofRead > 0); + + /* Only an emptied channel earns the EOF; anything + * else is retried on a later pass. */ + if (eofRead == 0) { + wolfSSH_ChannelSendEof(eofChannel); + eofAnswered = 1; + ChildRunning = 0; + } + } + } + if (cnt_r < 0) { rc = wolfSSH_get_error(ssh); if (rc == WS_CHAN_RXD) { @@ -1082,6 +1141,11 @@ static int ssh_worker(thread_ctx_t* threadCtx) #endif continue; } + else if (rc == WS_EOF) { + /* The half-close is answered by the durable check + * above, which has already run this pass. */ + continue; + } else if (rc != WS_WANT_READ) { #ifdef SHELL_DEBUG printf("Break:read sshFd returns %d: errno =%x\n", @@ -1379,6 +1443,8 @@ static int sftp_worker(thread_ctx_t* threadCtx) * if there is still pending sends */ } if (error == WS_EOF) { + /* An ordinary session end, not a failure. */ + ret = 0; break; } } @@ -1405,10 +1471,18 @@ static int sftp_worker(thread_ctx_t* threadCtx) ret = error; } + /* Drain what is buffered before leaving on the EOF. */ if (error == WS_EOF) { - break; + /* A rekey is not a drained channel. */ + int peekRet = wolfSSH_stream_peek(ssh, NULL, 1); + + if (peekRet != WS_REKEYING && peekRet <= 0) { + /* An ordinary session end, not a failure. */ + ret = 0; + break; + } } - if (ret != WS_SUCCESS && ret != WS_CHAN_RXD) { + if (ret != WS_SUCCESS && ret != WS_CHAN_RXD && ret != WS_EOF) { if (ret == WS_WANT_WRITE) { /* recall wolfSSH_worker here because is likely our custom * highwater callback that returned up a WS_WANT_WRITE */ @@ -1431,8 +1505,10 @@ static int sftp_worker(thread_ctx_t* threadCtx) error == WS_CHAN_RXD || error == WS_REKEYING || error == WS_WINDOW_FULL) ret = error; - if (error == WS_EOF) + if (error == WS_EOF) { + ret = 0; break; + } continue; } else if (ret == WS_REKEYING) { @@ -1441,8 +1517,11 @@ static int sftp_worker(thread_ctx_t* threadCtx) } else if (ret < 0) { error = wolfSSH_get_error(ssh); - if (error == WS_EOF) + if (error == WS_EOF) { + /* shutdown is happening, clear peek error */ + ret = 0; break; + } } if (ret == WS_FATAL_ERROR && error == 0) { @@ -1574,6 +1653,13 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs) ret = 0; } + /* The peer's close already retired the channel: a completed + * shutdown, not a failure. Left non-zero it sets quit, taking the + * server down after one session. */ + if (ret == WS_CHANNEL_CLOSED) { + ret = 0; + } + error = wolfSSH_get_error(threadCtx->ssh); if (error != WS_SOCKET_ERROR_E && (error == WS_WANT_READ || error == WS_WANT_WRITE)) { @@ -1585,7 +1671,7 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs) error = wolfSSH_get_error(threadCtx->ssh); /* peer succesfully closed down gracefully */ - if (ret == WS_CHANNEL_CLOSED) { + if (ret == WS_CHANNEL_CLOSED || ret == WS_EOF) { ret = 0; break; } diff --git a/ide/mplabx/wolfssh.c b/ide/mplabx/wolfssh.c index a2bbbd77..1adfde4b 100644 --- a/ide/mplabx/wolfssh.c +++ b/ide/mplabx/wolfssh.c @@ -841,8 +841,15 @@ void APP_Tasks ( void ) break; } + /* Drain what the peer sent before tearing down. A rekey is not + * a drained channel: peek reports it without looking. */ if (error == WS_EOF) { - appData.state = APP_SSH_CLEANUP; + int peekRet = wolfSSH_stream_peek(ssh, peek_buf, + sizeof(peek_buf)); + + if (peekRet != WS_REKEYING && peekRet <= 0) { + appData.state = APP_SSH_CLEANUP; + } break; }