mirror of https://github.com/wolfSSL/wolfssh.git
wolfssh/scp: complete rekey that starts mid-transfer
parent
7393aaea9b
commit
1efd64743c
263
src/wolfscp.c
263
src/wolfscp.c
|
|
@ -63,6 +63,17 @@ const char scpError[] = "scp error: %s, %d";
|
|||
const char scpState[] = "scp state: %s";
|
||||
|
||||
|
||||
/* Logs an SCP state error, except a non-blocking WS_WANT_READ/WS_WANT_WRITE,
|
||||
* which is normal back-pressure the caller retries (matching the quiet
|
||||
* SCP_SEND_FILE handling) rather than a transfer failure. */
|
||||
static void LogScpStateError(const char* state, int ret)
|
||||
{
|
||||
(void)state;
|
||||
if (ret != WS_WANT_READ && ret != WS_WANT_WRITE)
|
||||
WLOG(WS_LOG_ERROR, scpError, state, ret);
|
||||
}
|
||||
|
||||
|
||||
static int _DumpExtendedData(WOLFSSH* ssh)
|
||||
{
|
||||
byte msg[WOLFSSH_DEFAULT_EXTDATA_SZ];
|
||||
|
|
@ -79,6 +90,122 @@ static int _DumpExtendedData(WOLFSSH* ssh)
|
|||
}
|
||||
|
||||
|
||||
/* Sends sz bytes from data, completing any rekey or full window that blocks
|
||||
* the send.
|
||||
*
|
||||
* Attempts the send; on a WS_WINDOW_FULL (peer window reached zero) or
|
||||
* WS_REKEYING result, drives wolfSSH_worker once to pick up the peer's window
|
||||
* adjust or to advance the rekey, then retries the send. The retry is what
|
||||
* clears the status: wolfSSH_worker does not reset ssh->error, so a loop that
|
||||
* keyed on ssh->error staying WS_WINDOW_FULL would keep driving the worker
|
||||
* after the window already reopened and stall the transfer. This mirrors the
|
||||
* original SCP_SEND_FILE handling, extended to the control-message senders so a
|
||||
* rekey or full window during a timestamp/header/confirmation send is no longer
|
||||
* reported as fatal. Termination needs no retry count: in blocking mode
|
||||
* wolfSSH_worker waits on the socket for the peer's packet; in non-blocking
|
||||
* mode it returns WS_WANT_READ/WS_WANT_WRITE, which is returned to the caller
|
||||
* so a stalled rekey cannot spin forever. Returns the byte count from
|
||||
* wolfSSH_stream_send (>= 0) or a negative error code, matching the wrapped
|
||||
* call so callers keep their existing return handling.
|
||||
*/
|
||||
static int ScpStreamSend(WOLFSSH* ssh, byte* data, word32 sz)
|
||||
{
|
||||
int ret = WS_SUCCESS;
|
||||
int err;
|
||||
int done = 0;
|
||||
|
||||
if (ssh == NULL || data == NULL)
|
||||
return WS_BAD_ARGUMENT;
|
||||
|
||||
/* Flush queued output before sending. Otherwise a KEXINIT enqueued by a
|
||||
* rekey triggered on the prior send sits unsent while wolfSSH_worker runs
|
||||
* DoReceive before its own flush, blocking on the socket with the peer
|
||||
* waiting for our KEXINIT. */
|
||||
if (wolfSSH_OutputPending(ssh)) {
|
||||
ret = wolfSSH_SendPacket(ssh);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
}
|
||||
|
||||
while (!done) {
|
||||
ret = wolfSSH_stream_send(ssh, data, sz);
|
||||
if (ret >= 0) {
|
||||
/* sent (full or partial byte count); exits via while (!done) */
|
||||
done = 1;
|
||||
}
|
||||
else {
|
||||
err = wolfSSH_get_error(ssh);
|
||||
if (err == WS_WINDOW_FULL || err == WS_REKEYING) {
|
||||
ret = wolfSSH_worker(ssh, NULL);
|
||||
err = wolfSSH_get_error(ssh);
|
||||
/* A non-blocking want surfaces as a generic worker error with
|
||||
* the want recorded in ssh->error (see GetInputData). Return it
|
||||
* so the caller retries instead of tearing down the send. */
|
||||
if (err == WS_WANT_READ || err == WS_WANT_WRITE)
|
||||
return err;
|
||||
/* Only a rekey/window/channel-data status means "keep driving".
|
||||
* Any other negative status is fatal and returned. */
|
||||
if (ret < 0 && ret != WS_REKEYING && ret != WS_WINDOW_FULL
|
||||
&& ret != WS_CHAN_RXD)
|
||||
return ret;
|
||||
/* otherwise loop and retry the send, which clears the status */
|
||||
}
|
||||
else {
|
||||
/* Fatal or other final status; return it unchanged. */
|
||||
done = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/* Reads up to sz bytes into data, completing any rekey that fires mid-read.
|
||||
*
|
||||
* Flushes queued output before reading so a KEXINIT enqueued by a receive-side
|
||||
* highwater rekey is actually sent, otherwise the peer can wait for our KEXINIT
|
||||
* while we block on the read. On a read that fails with WS_REKEYING the worker
|
||||
* is driven to finish the rekey and the read is retried. The helper is
|
||||
* error-code transparent: every other status (WS_EOF, WS_EXTDATA,
|
||||
* WS_CHANNEL_CLOSED, WS_SOCKET_ERROR_E, WS_WANT_READ/WS_WANT_WRITE, byte count)
|
||||
* is returned unchanged so each caller keeps its existing branch handling.
|
||||
*/
|
||||
static int ScpStreamRead(WOLFSSH* ssh, byte* data, word32 sz)
|
||||
{
|
||||
int ret = WS_SUCCESS;
|
||||
int done = 0;
|
||||
|
||||
if (ssh == NULL || data == NULL)
|
||||
return WS_BAD_ARGUMENT;
|
||||
|
||||
do {
|
||||
if (wolfSSH_OutputPending(ssh)) {
|
||||
ret = wolfSSH_SendPacket(ssh);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = wolfSSH_stream_read(ssh, data, sz);
|
||||
if (ret < 0 && wolfSSH_get_error(ssh) == WS_REKEYING) {
|
||||
/* Drive the rekey to completion, then retry the read. A worker
|
||||
* status that is not rekey or channel data means the rekey stalled
|
||||
* or a non-blocking want occurred, so return it rather than
|
||||
* spin. */
|
||||
ret = wolfSSH_worker(ssh, NULL);
|
||||
if (ret < 0 && ret != WS_CHAN_RXD
|
||||
&& wolfSSH_get_error(ssh) != WS_REKEYING)
|
||||
return ret;
|
||||
}
|
||||
else {
|
||||
done = 1;
|
||||
}
|
||||
} while (!done);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
int DoScpSink(WOLFSSH* ssh)
|
||||
{
|
||||
int ret = WS_SUCCESS;
|
||||
|
|
@ -112,7 +239,7 @@ int DoScpSink(WOLFSSH* ssh)
|
|||
break;
|
||||
}
|
||||
|
||||
WLOG(WS_LOG_ERROR, scpError, "RECEIVE_MESSAGE", ret);
|
||||
LogScpStateError("RECEIVE_MESSAGE", ret);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -153,7 +280,7 @@ int DoScpSink(WOLFSSH* ssh)
|
|||
WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_CONFIRMATION");
|
||||
|
||||
if ( (ret = SendScpConfirmation(ssh)) < WS_SUCCESS) {
|
||||
WLOG(WS_LOG_ERROR, scpError, "SEND_CONFIRMATION", ret);
|
||||
LogScpStateError("SEND_CONFIRMATION", ret);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -164,7 +291,7 @@ int DoScpSink(WOLFSSH* ssh)
|
|||
WLOG(WS_LOG_DEBUG, scpState, "SCP_RECEIVE_CONFIRMATION");
|
||||
|
||||
if ( (ret = ReceiveScpConfirmation(ssh)) < WS_SUCCESS) {
|
||||
WLOG(WS_LOG_ERROR, scpError, "RECEIVE_CONFIRMATION", ret);
|
||||
LogScpStateError("RECEIVE_CONFIRMATION", ret);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +302,7 @@ int DoScpSink(WOLFSSH* ssh)
|
|||
WLOG(WS_LOG_DEBUG, scpState, "SCP_RECEIVE_FILE");
|
||||
|
||||
if ( (ret = ReceiveScpFile(ssh)) < WS_SUCCESS) {
|
||||
WLOG(WS_LOG_ERROR, scpError, "RECEIVE_FILE", ret);
|
||||
LogScpStateError("RECEIVE_FILE", ret);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -283,13 +410,17 @@ static int SendScpTimestamp(WOLFSSH* ssh)
|
|||
#endif
|
||||
bufSz = (int)WSTRLEN(buf);
|
||||
|
||||
ret = wolfSSH_stream_send(ssh, (byte*)buf, bufSz);
|
||||
if (ret != bufSz) {
|
||||
ret = WS_FATAL_ERROR;
|
||||
} else {
|
||||
ret = ScpStreamSend(ssh, (byte*)buf, bufSz);
|
||||
if (ret == bufSz) {
|
||||
WLOG(WS_LOG_DEBUG, "scp: sent timestamp: %s", buf);
|
||||
ret = WS_SUCCESS;
|
||||
}
|
||||
/* A non-blocking want is left as WS_WANT_READ/WS_WANT_WRITE for the caller
|
||||
* to retry (nothing is queued yet, so the resend is clean), consistent with
|
||||
* the SCP_SEND_FILE data path; only a real short send is fatal. */
|
||||
else if (ret != WS_WANT_READ && ret != WS_WANT_WRITE) {
|
||||
ret = WS_FATAL_ERROR;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
@ -324,13 +455,16 @@ static int SendScpFileHeader(WOLFSSH* ssh)
|
|||
return WS_BAD_ARGUMENT;
|
||||
#endif
|
||||
bufSz = (int)WSTRLEN(filehdr);
|
||||
ret = wolfSSH_stream_send(ssh, (byte*)filehdr, bufSz);
|
||||
if (ret != bufSz) {
|
||||
ret = WS_FATAL_ERROR;
|
||||
} else {
|
||||
ret = ScpStreamSend(ssh, (byte*)filehdr, bufSz);
|
||||
if (ret == bufSz) {
|
||||
WLOG(WS_LOG_DEBUG, "scp: sent file header: %s", filehdr);
|
||||
ret = WS_SUCCESS;
|
||||
}
|
||||
/* leave a non-blocking want for the caller to retry; only a real short
|
||||
* send is fatal (see SendScpTimestamp) */
|
||||
else if (ret != WS_WANT_READ && ret != WS_WANT_WRITE) {
|
||||
ret = WS_FATAL_ERROR;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
|
@ -357,13 +491,16 @@ static int SendScpEnterDirectory(WOLFSSH* ssh)
|
|||
|
||||
bufSz = (int)WSTRLEN(buf);
|
||||
|
||||
ret = wolfSSH_stream_send(ssh, (byte*)buf, bufSz);
|
||||
if (ret != bufSz) {
|
||||
ret = WS_FATAL_ERROR;
|
||||
} else {
|
||||
ret = ScpStreamSend(ssh, (byte*)buf, bufSz);
|
||||
if (ret == bufSz) {
|
||||
WLOG(WS_LOG_DEBUG, "scp: sent directory msg: %s", buf);
|
||||
ret = WS_SUCCESS;
|
||||
}
|
||||
/* leave a non-blocking want for the caller to retry; only a real short
|
||||
* send is fatal (see SendScpTimestamp) */
|
||||
else if (ret != WS_WANT_READ && ret != WS_WANT_WRITE) {
|
||||
ret = WS_FATAL_ERROR;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
@ -383,13 +520,16 @@ static int SendScpExitDirectory(WOLFSSH* ssh)
|
|||
buf[0] = 'E';
|
||||
buf[1] = '\n';
|
||||
|
||||
ret = wolfSSH_stream_send(ssh, (byte*)buf, sizeof(buf));
|
||||
if (ret != sizeof(buf)) {
|
||||
ret = WS_FATAL_ERROR;
|
||||
} else {
|
||||
ret = ScpStreamSend(ssh, (byte*)buf, sizeof(buf));
|
||||
if (ret == sizeof(buf)) {
|
||||
WLOG(WS_LOG_DEBUG, "scp: sent end directory msg: E");
|
||||
ret = WS_SUCCESS;
|
||||
}
|
||||
/* leave a non-blocking want for the caller to retry; only a real short
|
||||
* send is fatal (see SendScpTimestamp) */
|
||||
else if (ret != WS_WANT_READ && ret != WS_WANT_WRITE) {
|
||||
ret = WS_FATAL_ERROR;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
@ -436,7 +576,7 @@ int DoScpSource(WOLFSSH* ssh)
|
|||
WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_CONFIRMATION");
|
||||
|
||||
if ( (ret = SendScpConfirmation(ssh)) < WS_SUCCESS) {
|
||||
WLOG(WS_LOG_ERROR, scpError, "SEND_CONFIRMATION", ret);
|
||||
LogScpStateError("SEND_CONFIRMATION", ret);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -447,7 +587,7 @@ int DoScpSource(WOLFSSH* ssh)
|
|||
WLOG(WS_LOG_DEBUG, scpState, "SCP_CONFIRMATION_WITH_RECEIPT");
|
||||
|
||||
if ( (ret = SendScpConfirmation(ssh)) < WS_SUCCESS) {
|
||||
WLOG(WS_LOG_ERROR, scpError, "SEND_CONFIRMATION", ret);
|
||||
LogScpStateError("SEND_CONFIRMATION", ret);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -459,8 +599,7 @@ int DoScpSource(WOLFSSH* ssh)
|
|||
"SCP_RECEIVE_CONFIRMATION_WITH_RECEIPT");
|
||||
|
||||
if ( (ret = ReceiveScpConfirmation(ssh)) < WS_SUCCESS) {
|
||||
WLOG(WS_LOG_ERROR, scpError,
|
||||
"RECEIVE_CONFIRMATION_WITH_RECEIPT", ret);
|
||||
LogScpStateError("RECEIVE_CONFIRMATION_WITH_RECEIPT", ret);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -471,7 +610,7 @@ int DoScpSource(WOLFSSH* ssh)
|
|||
WLOG(WS_LOG_DEBUG, scpState, "SCP_RECEIVE_CONFIRMATION");
|
||||
|
||||
if ( (ret = ReceiveScpConfirmation(ssh)) < WS_SUCCESS) {
|
||||
WLOG(WS_LOG_ERROR, scpError, "RECEIVE_CONFIRMATION", ret);
|
||||
LogScpStateError("RECEIVE_CONFIRMATION", ret);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -540,7 +679,7 @@ int DoScpSource(WOLFSSH* ssh)
|
|||
WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_TIMESTAMP");
|
||||
|
||||
if ( (ret = SendScpTimestamp(ssh)) < WS_SUCCESS) {
|
||||
WLOG(WS_LOG_ERROR, scpError, "SEND_TIMESTAMP", ret);
|
||||
LogScpStateError("SEND_TIMESTAMP", ret);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -552,7 +691,7 @@ int DoScpSource(WOLFSSH* ssh)
|
|||
WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_ENTER_DIRECTORY");
|
||||
|
||||
if ( (ret = SendScpEnterDirectory(ssh)) < WS_SUCCESS) {
|
||||
WLOG(WS_LOG_ERROR, scpError, "SEND_ENTER_DIRECTORY", ret);
|
||||
LogScpStateError("SEND_ENTER_DIRECTORY", ret);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -564,7 +703,7 @@ int DoScpSource(WOLFSSH* ssh)
|
|||
WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_EXIT_DIRECTORY");
|
||||
|
||||
if ( (ret = SendScpExitDirectory(ssh)) < WS_SUCCESS) {
|
||||
WLOG(WS_LOG_ERROR, scpError, "SEND_EXIT_DIRECTORY", ret);
|
||||
LogScpStateError("SEND_EXIT_DIRECTORY", ret);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -576,7 +715,7 @@ int DoScpSource(WOLFSSH* ssh)
|
|||
WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_EXIT_DIRECTORY_FINAL");
|
||||
|
||||
if ( (ret = SendScpExitDirectory(ssh)) < WS_SUCCESS) {
|
||||
WLOG(WS_LOG_ERROR, scpError, "SEND_EXIT_DIRECTORY", ret);
|
||||
LogScpStateError("SEND_EXIT_DIRECTORY", ret);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -587,7 +726,7 @@ int DoScpSource(WOLFSSH* ssh)
|
|||
WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_FILE_HEADER");
|
||||
|
||||
if ( (ret = SendScpFileHeader(ssh)) < WS_SUCCESS) {
|
||||
WLOG(WS_LOG_ERROR, scpError, "SEND_FILE_HEADER", ret);
|
||||
LogScpStateError("SEND_FILE_HEADER", ret);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -598,12 +737,15 @@ int DoScpSource(WOLFSSH* ssh)
|
|||
case SCP_SEND_FILE:
|
||||
WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_FILE");
|
||||
|
||||
ret = wolfSSH_stream_send(ssh, ssh->scpFileBuffer,
|
||||
ssh->scpBufferedSz);
|
||||
if (ret == WS_WINDOW_FULL || ret == WS_REKEYING) {
|
||||
ret = wolfSSH_worker(ssh, NULL);
|
||||
if (ret == WS_SUCCESS || ssh->error == WS_WANT_READ)
|
||||
continue;
|
||||
ret = ScpStreamSend(ssh, ssh->scpFileBuffer,
|
||||
ssh->scpBufferedSz);
|
||||
if (ret == WS_WANT_READ || ret == WS_WANT_WRITE) {
|
||||
/* ScpStreamSend already drove the worker through any rekey
|
||||
* or full window; a non-blocking want means the socket is
|
||||
* not ready. Surface it for the caller to retry without
|
||||
* closing the file mid-transfer. scpBufferedSz and
|
||||
* scpFileOffset are preserved for the next call. */
|
||||
break;
|
||||
}
|
||||
if (ret == WS_EXTDATA) {
|
||||
_DumpExtendedData(ssh);
|
||||
|
|
@ -718,14 +860,14 @@ int DoScpRequest(WOLFSSH* ssh)
|
|||
case SCP_SINK:
|
||||
WLOG(WS_LOG_DEBUG, scpState, "SCP_SINK");
|
||||
if ( (ret = DoScpSink(ssh)) < WS_SUCCESS) {
|
||||
WLOG(WS_LOG_ERROR, scpError, "SCP_SINK", ret);
|
||||
LogScpStateError("SCP_SINK", ret);
|
||||
}
|
||||
break;
|
||||
|
||||
case SCP_SOURCE:
|
||||
WLOG(WS_LOG_DEBUG, scpState, "SCP_SOURCE");
|
||||
if ( (ret = DoScpSource(ssh)) < WS_SUCCESS) {
|
||||
WLOG(WS_LOG_ERROR, scpError, "SCP_SOURCE", ret);
|
||||
LogScpStateError("SCP_SOURCE", ret);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -736,7 +878,7 @@ int DoScpRequest(WOLFSSH* ssh)
|
|||
|
||||
/* Peer MUST send back a SSH_MSG_CHANNEL_CLOSE unless already
|
||||
sent*/
|
||||
ret = wolfSSH_stream_read(ssh, buf, 1);
|
||||
ret = ScpStreamRead(ssh, buf, 1);
|
||||
if (ret == WS_SOCKET_ERROR_E || ret == WS_CHANNEL_CLOSED) {
|
||||
WLOG(WS_LOG_DEBUG, scpState, "Peer hung up, but SCP is done");
|
||||
ret = WS_SUCCESS;
|
||||
|
|
@ -1395,6 +1537,33 @@ int ReceiveScpMessage(WOLFSSH* ssh)
|
|||
return WS_BUFFER_E;
|
||||
}
|
||||
|
||||
/* Flush queued output before polling. A KEXINIT enqueued by a rekey
|
||||
* would otherwise sit unsent while wolfSSH_worker runs DoReceive before
|
||||
* its own flush, deadlocking against a peer that waits for our
|
||||
* KEXINIT. */
|
||||
if (wolfSSH_OutputPending(ssh)) {
|
||||
ret = wolfSSH_SendPacket(ssh);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* If channel data is already buffered, read it directly rather than
|
||||
* polling the socket. A control message delivered into the channel
|
||||
* buffer while a rekey was completing leaves wolfSSH_worker returning
|
||||
* the rekey status (not WS_CHAN_RXD), so without this the buffered
|
||||
* message is never read and the next worker blocks on the socket. */
|
||||
if (wolfSSH_stream_peek(ssh, NULL, 1) > 0) {
|
||||
sz = wolfSSH_stream_read(ssh, buf + ssh->scpRecvMsgSz,
|
||||
DEFAULT_SCP_MSG_SZ - ssh->scpRecvMsgSz);
|
||||
/* match the WS_CHAN_RXD branch below: return on a non-positive
|
||||
* read so a hypothetical zero cannot re-loop this peek path */
|
||||
if (sz <= 0)
|
||||
return sz;
|
||||
ssh->scpRecvMsgSz += sz;
|
||||
sz = ssh->scpRecvMsgSz;
|
||||
continue;
|
||||
}
|
||||
|
||||
err = wolfSSH_worker(ssh, &lastChannel);
|
||||
if (err < 0) {
|
||||
int rc;
|
||||
|
|
@ -1517,7 +1686,7 @@ int ReceiveScpFile(WOLFSSH* ssh)
|
|||
}
|
||||
|
||||
if (ret == WS_SUCCESS) {
|
||||
ret = wolfSSH_stream_read(ssh, ssh->scpFileBuffer, partSz);
|
||||
ret = ScpStreamRead(ssh, ssh->scpFileBuffer, partSz);
|
||||
if (ret > 0) {
|
||||
ssh->scpFileBufferSz = ret;
|
||||
}
|
||||
|
|
@ -1559,11 +1728,8 @@ int SendScpConfirmation(WOLFSSH* ssh)
|
|||
|
||||
/* skip first byte for accurate strlen, may be 0 */
|
||||
msgSz = (int)XSTRLEN(msg + 1) + 1;
|
||||
ret = wolfSSH_stream_send(ssh, (byte*)msg, msgSz);
|
||||
if (ret != msgSz || ssh->scpConfirm == WS_SCP_ABORT) {
|
||||
ret = WS_FATAL_ERROR;
|
||||
|
||||
} else {
|
||||
ret = ScpStreamSend(ssh, (byte*)msg, msgSz);
|
||||
if (ret == msgSz && ssh->scpConfirm != WS_SCP_ABORT) {
|
||||
ret = WS_SUCCESS;
|
||||
WLOG(WS_LOG_DEBUG, "scp: sent confirmation (code: %d)", msg[0]);
|
||||
|
||||
|
|
@ -1573,6 +1739,11 @@ int SendScpConfirmation(WOLFSSH* ssh)
|
|||
ssh->scpConfirmMsgSz = 0;
|
||||
}
|
||||
}
|
||||
/* leave a non-blocking want for the caller to retry; a real short send or a
|
||||
* peer abort is fatal (see SendScpTimestamp) */
|
||||
else if (ret != WS_WANT_READ && ret != WS_WANT_WRITE) {
|
||||
ret = WS_FATAL_ERROR;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
@ -1587,7 +1758,7 @@ int ReceiveScpConfirmation(WOLFSSH* ssh)
|
|||
return WS_BAD_ARGUMENT;
|
||||
|
||||
WMEMSET(msg, 0, sizeof(msg));
|
||||
msgSz = wolfSSH_stream_read(ssh, msg, DEFAULT_SCP_MSG_SZ);
|
||||
msgSz = ScpStreamRead(ssh, msg, DEFAULT_SCP_MSG_SZ);
|
||||
|
||||
if (msgSz < 0) {
|
||||
if (wolfSSH_get_error(ssh) == WS_EXTDATA)
|
||||
|
|
|
|||
386
tests/api.c
386
tests/api.c
|
|
@ -49,7 +49,7 @@
|
|||
#include <wolfssh/agent.h>
|
||||
#endif
|
||||
|
||||
#ifdef WOLFSSH_SFTP
|
||||
#if defined(WOLFSSH_SFTP) || defined(WOLFSSH_SCP)
|
||||
#define WOLFSSH_TEST_LOCKING
|
||||
#ifndef SINGLE_THREADED
|
||||
#define WOLFSSH_TEST_THREADING
|
||||
|
|
@ -62,6 +62,9 @@
|
|||
#endif
|
||||
#include <wolfssh/test.h>
|
||||
#include "tests/api.h"
|
||||
#ifdef WOLFSSH_TEST_ECHOSERVER
|
||||
#include "examples/echoserver/echoserver.h"
|
||||
#endif
|
||||
|
||||
/* for echoserver test cases */
|
||||
int myoptind = 0;
|
||||
|
|
@ -1249,8 +1252,6 @@ static void test_wolfSSH_agent_signrequest_success(void)
|
|||
#if defined(WOLFSSH_SFTP) && !defined(NO_WOLFSSH_CLIENT) && \
|
||||
!defined(SINGLE_THREADED)
|
||||
|
||||
#include "examples/echoserver/echoserver.h"
|
||||
|
||||
byte userPassword[256];
|
||||
|
||||
static int sftpUserAuth(byte authType, WS_UserAuthData* authData, void* ctx)
|
||||
|
|
@ -1288,6 +1289,14 @@ static int AcceptAnyServerHostKey(const byte* pubKey, word32 pubKeySz,
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* Counts keying completions (initial handshake and each rekey) so the test can
|
||||
* assert a mid-transfer rekey actually fired. ctx points to an int counter. */
|
||||
static void sftpKeyingCompleteCb(void* ctx)
|
||||
{
|
||||
if (ctx != NULL)
|
||||
(*(int*)ctx)++;
|
||||
}
|
||||
|
||||
/* performs connection to port, sets WOLFSSH_CTX and WOLFSSH on success
|
||||
* caller needs to free ctx and ssh when done
|
||||
*/
|
||||
|
|
@ -1870,6 +1879,7 @@ static void sftp_rekey_test(int nonBlock)
|
|||
int argsCount;
|
||||
int err;
|
||||
int tries;
|
||||
int kexCount = 0;
|
||||
WS_SOCKET_T clientFd;
|
||||
WS_SFTPNAME* ls;
|
||||
int i;
|
||||
|
|
@ -1900,6 +1910,12 @@ static void sftp_rekey_test(int nonBlock)
|
|||
AssertNotNull(ctx);
|
||||
AssertNotNull(ssh);
|
||||
|
||||
/* Count keying completions from here on. The initial handshake already ran
|
||||
* inside sftp_client_connect, so kexCount stays 0 until the highwater rekey
|
||||
* fires mid-SFTP; the AssertIntGT below then proves it did. */
|
||||
wolfSSH_SetKeyingCompletionCb(ctx, sftpKeyingCompleteCb);
|
||||
wolfSSH_SetKeyingCompletionCbCtx(ssh, &kexCount);
|
||||
|
||||
/* Handshake completed in blocking mode; switch to non-blocking so the
|
||||
* LS/rekey phase exercises the WS_WANT_READ/WS_WANT_WRITE early-return
|
||||
* path in buffer_send/buffer_read. */
|
||||
|
|
@ -1946,6 +1962,10 @@ static void sftp_rekey_test(int nonBlock)
|
|||
ls = NULL;
|
||||
}
|
||||
|
||||
/* A mid-SFTP rekey must have fired; otherwise the test silently stops
|
||||
* exercising the buffer_send/buffer_read rekey paths it was written for. */
|
||||
AssertIntGT(kexCount, 0);
|
||||
|
||||
tries = 0;
|
||||
do {
|
||||
argsCount = wolfSSH_shutdown(ssh);
|
||||
|
|
@ -2454,6 +2474,362 @@ static void test_wolfSSH_SFTP_SetDefaultPath(void) { ; }
|
|||
#endif /* WOLFSSH_SFTP && !NO_WOLFSSH_CLIENT && !SINGLE_THREADED */
|
||||
|
||||
|
||||
#if defined(WOLFSSH_SCP) && !defined(NO_WOLFSSH_CLIENT) && \
|
||||
!defined(SINGLE_THREADED) && !defined(NO_FILESYSTEM) && \
|
||||
!defined(WOLFSSH_SCP_USER_CALLBACKS) && !defined(WOLFSSH_ZEPHYR)
|
||||
|
||||
/* Upper bound on non-blocking retry iterations. A legitimate transfer across a
|
||||
* forced rekey completes in well under this; the bound keeps a regression from
|
||||
* hanging CI by tripping the AssertIntLE below instead. */
|
||||
#define SCP_REKEY_MAX_TRIES 100
|
||||
|
||||
/* Payload larger than the forced highwater so the transfer straddles it. */
|
||||
#define SCP_REKEY_FILE_SZ 2048
|
||||
|
||||
static byte scpUserPassword[256];
|
||||
|
||||
static int scpUserAuth(byte authType, WS_UserAuthData* authData, void* ctx)
|
||||
{
|
||||
int ret = WOLFSSH_USERAUTH_INVALID_AUTHTYPE;
|
||||
|
||||
if (authType == WOLFSSH_USERAUTH_PASSWORD) {
|
||||
const char* password = (const char*)ctx;
|
||||
word32 passwordSz;
|
||||
|
||||
if (password != NULL) {
|
||||
passwordSz = (word32)WSTRLEN(password);
|
||||
if (passwordSz > (word32)sizeof(scpUserPassword))
|
||||
passwordSz = (word32)sizeof(scpUserPassword);
|
||||
WMEMCPY(scpUserPassword, password, passwordSz);
|
||||
authData->sf.password.password = scpUserPassword;
|
||||
authData->sf.password.passwordSz = passwordSz;
|
||||
ret = WOLFSSH_USERAUTH_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int scpAcceptAnyServerHostKey(const byte* pubKey, word32 pubKeySz,
|
||||
void* ctx)
|
||||
{
|
||||
(void)pubKey;
|
||||
(void)pubKeySz;
|
||||
(void)ctx;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Counts keying completions (initial handshake and each rekey) so the test can
|
||||
* assert a mid-transfer rekey actually fired. ctx points to an int counter. */
|
||||
static void scpKeyingCompleteCb(void* ctx)
|
||||
{
|
||||
if (ctx != NULL)
|
||||
(*(int*)ctx)++;
|
||||
}
|
||||
|
||||
/* Writes sz bytes of buf to name. Returns 0 on success. */
|
||||
static int scpWriteTestFile(const char* name, const byte* buf, word32 sz)
|
||||
{
|
||||
WFILE* fp = NULL;
|
||||
int ret = 0;
|
||||
|
||||
if (WFOPEN(NULL, &fp, name, "wb") != 0 || fp == NULL)
|
||||
return -1;
|
||||
|
||||
if (WFWRITE(NULL, buf, 1, sz, fp) != sz)
|
||||
ret = -1;
|
||||
|
||||
WFCLOSE(NULL, fp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Returns 0 if the first sz bytes of name match expect. */
|
||||
static int scpFilesMatch(const char* name, const byte* expect, word32 sz)
|
||||
{
|
||||
WFILE* fp = NULL;
|
||||
byte got[SCP_REKEY_FILE_SZ];
|
||||
int ret = 0;
|
||||
|
||||
if (sz > sizeof(got))
|
||||
return -1;
|
||||
|
||||
if (WFOPEN(NULL, &fp, name, "rb") != 0 || fp == NULL)
|
||||
return -1;
|
||||
|
||||
if (WFREAD(NULL, got, 1, sz, fp) != sz)
|
||||
ret = -1;
|
||||
|
||||
if (ret == 0 && XMEMCMP(got, expect, sz) != 0)
|
||||
ret = -1;
|
||||
|
||||
WFCLOSE(NULL, fp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Connects an SCP client to port, completes the SSH handshake and opens the
|
||||
* exec channel carrying cmd, leaving ssh ready for wolfSSH_SCP_to/from. Doing
|
||||
* the handshake here (rather than inside the transfer call) lets the caller set
|
||||
* a low highwater before the data phase so a rekey fires mid-transfer.
|
||||
*/
|
||||
static void scp_client_connect(WOLFSSH_CTX** ctx, WOLFSSH** ssh, int port,
|
||||
const char* cmd)
|
||||
{
|
||||
WS_SOCKET_T sockFd = WOLFSSH_SOCKET_INVALID;
|
||||
SOCKADDR_IN_T clientAddr;
|
||||
socklen_t clientAddrSz = sizeof(clientAddr);
|
||||
int ret;
|
||||
char* host = (char*)wolfSshIp;
|
||||
const char* username = "jill";
|
||||
const char* password = "upthehill";
|
||||
|
||||
if (ctx == NULL || ssh == NULL)
|
||||
return;
|
||||
|
||||
*ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
|
||||
if (*ctx == NULL)
|
||||
return;
|
||||
|
||||
wolfSSH_CTX_SetPublicKeyCheck(*ctx, scpAcceptAnyServerHostKey);
|
||||
wolfSSH_SetUserAuth(*ctx, scpUserAuth);
|
||||
*ssh = wolfSSH_new(*ctx);
|
||||
if (*ssh == NULL) {
|
||||
wolfSSH_CTX_free(*ctx);
|
||||
*ctx = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
build_addr(&clientAddr, host, port);
|
||||
tcp_socket(&sockFd, ((struct sockaddr_in *)&clientAddr)->sin_family);
|
||||
if (sockFd < 0) {
|
||||
wolfSSH_free(*ssh);
|
||||
wolfSSH_CTX_free(*ctx);
|
||||
*ctx = NULL;
|
||||
*ssh = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
ret = connect(sockFd, (const struct sockaddr *)&clientAddr, clientAddrSz);
|
||||
if (ret != 0) {
|
||||
WCLOSESOCKET(sockFd);
|
||||
wolfSSH_free(*ssh);
|
||||
wolfSSH_CTX_free(*ctx);
|
||||
*ctx = NULL;
|
||||
*ssh = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
wolfSSH_SetUserAuthCtx(*ssh, (void*)password);
|
||||
ret = wolfSSH_SetUsername(*ssh, username);
|
||||
if (ret == WS_SUCCESS)
|
||||
ret = wolfSSH_SetChannelType(*ssh, WOLFSSH_SESSION_EXEC, (byte*)cmd,
|
||||
(word32)WSTRLEN(cmd));
|
||||
if (ret == WS_SUCCESS)
|
||||
ret = wolfSSH_set_fd(*ssh, (int)sockFd);
|
||||
if (ret == WS_SUCCESS)
|
||||
ret = wolfSSH_connect(*ssh);
|
||||
|
||||
if (ret != WS_SUCCESS) {
|
||||
WCLOSESOCKET(sockFd);
|
||||
wolfSSH_free(*ssh);
|
||||
wolfSSH_CTX_free(*ctx);
|
||||
*ctx = NULL;
|
||||
*ssh = NULL;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* Drives an SCP transfer with a forced mid-transfer rekey.
|
||||
*
|
||||
* toServer == 0: client SINK (wolfSSH_SCP_from), exercises ScpStreamRead, the
|
||||
* confirmed hang path. toServer == 1: client SOURCE
|
||||
* (wolfSSH_SCP_to), exercises the ScpStreamSend rekey/window
|
||||
* drain loop. nonBlock drives the non-blocking retry path.
|
||||
*/
|
||||
static void scp_rekey_test(int nonBlock, int toServer)
|
||||
{
|
||||
func_args ser;
|
||||
tcp_ready ready;
|
||||
int argsCount;
|
||||
int ret;
|
||||
int err;
|
||||
int tries;
|
||||
int kexCount = 0;
|
||||
word32 i;
|
||||
WS_SOCKET_T clientFd;
|
||||
#ifdef USE_WINDOWS_API
|
||||
DWORD rcvTimeout = 20000;
|
||||
#else
|
||||
struct timeval rcvTimeout;
|
||||
#endif
|
||||
byte fileData[SCP_REKEY_FILE_SZ];
|
||||
char cmd[64];
|
||||
const char* args[10];
|
||||
WOLFSSH_CTX* ctx = NULL;
|
||||
WOLFSSH* ssh = NULL;
|
||||
/* Fixed names used for filesystem create/verify/cleanup. The *Buf copies
|
||||
* are what get passed to the SCP API, which rewrites the path in place
|
||||
* (rename/clean), so they cannot be reused to name the file afterward. The
|
||||
* leading "./" keeps a directory component so the base-dir open succeeds,
|
||||
* as the real scpclient passes $PWD-prefixed paths. */
|
||||
const char* srcName = "./scp_rekey_src.txt";
|
||||
const char* fromName = "./scp_rekey_from.txt";
|
||||
const char* toName = "./scp_rekey_to.txt";
|
||||
char srcBuf[32];
|
||||
char fromBuf[32];
|
||||
char toBuf[32];
|
||||
const char* verifyName;
|
||||
|
||||
THREAD_TYPE serThread;
|
||||
|
||||
/* mutable copies for the SCP API (rewritten in place during the transfer) */
|
||||
WSTRNCPY(srcBuf, srcName, sizeof(srcBuf));
|
||||
WSTRNCPY(fromBuf, fromName, sizeof(fromBuf));
|
||||
WSTRNCPY(toBuf, toName, sizeof(toBuf));
|
||||
|
||||
/* deterministic source content */
|
||||
for (i = 0; i < SCP_REKEY_FILE_SZ; i++)
|
||||
fileData[i] = (byte)(i & 0xff);
|
||||
AssertIntEQ(scpWriteTestFile(srcName, fileData, SCP_REKEY_FILE_SZ), 0);
|
||||
|
||||
WMEMSET(&ser, 0, sizeof(func_args));
|
||||
argsCount = 0;
|
||||
args[argsCount++] = ".";
|
||||
args[argsCount++] = "-1";
|
||||
#ifndef USE_WINDOWS_API
|
||||
args[argsCount++] = "-p";
|
||||
args[argsCount++] = "0";
|
||||
#endif
|
||||
ser.argv = (char**)args;
|
||||
ser.argc = argsCount;
|
||||
ser.signal = &ready;
|
||||
InitTcpReady(ser.signal);
|
||||
ThreadStart(echoserver_test, (void*)&ser, &serThread);
|
||||
WaitTcpReady(&ready);
|
||||
|
||||
/* -f: server is source (client SINK); -t: server is sink (client SOURCE) */
|
||||
if (toServer) {
|
||||
WSNPRINTF(cmd, sizeof(cmd), "scp -t %s", toName);
|
||||
verifyName = toName;
|
||||
}
|
||||
else {
|
||||
WSNPRINTF(cmd, sizeof(cmd), "scp -f %s", srcName);
|
||||
verifyName = fromName;
|
||||
}
|
||||
|
||||
scp_client_connect(&ctx, &ssh, ready.port, cmd);
|
||||
AssertNotNull(ctx);
|
||||
AssertNotNull(ssh);
|
||||
|
||||
/* Count keying completions from here on. The initial handshake already ran
|
||||
* inside scp_client_connect, so kexCount stays 0 until the highwater-driven
|
||||
* rekey fires mid-transfer; the AssertIntGT below then proves it did. */
|
||||
wolfSSH_SetKeyingCompletionCb(ctx, scpKeyingCompleteCb);
|
||||
wolfSSH_SetKeyingCompletionCbCtx(ssh, &kexCount);
|
||||
|
||||
/* handshake done in blocking mode; switch to non-blocking for the data
|
||||
* phase so the WS_WANT_READ/WS_WANT_WRITE retry path is exercised */
|
||||
clientFd = wolfSSH_get_fd(ssh);
|
||||
if (nonBlock)
|
||||
tcp_set_nonblocking(&clientFd);
|
||||
|
||||
/* Bound the blocking-mode recv so a KEXINIT/rekey deadlock regression fails
|
||||
* the AssertIntEQ below instead of hanging CI forever. The
|
||||
* SCP_REKEY_MAX_TRIES bound only covers the non-blocking retry loop; a
|
||||
* non-blocking socket never blocks in recv, so this is a no-op there. */
|
||||
#ifdef USE_WINDOWS_API
|
||||
(void)setsockopt(clientFd, SOL_SOCKET, SO_RCVTIMEO,
|
||||
(const char*)&rcvTimeout, sizeof(rcvTimeout));
|
||||
#else
|
||||
rcvTimeout.tv_sec = 20;
|
||||
rcvTimeout.tv_usec = 0;
|
||||
(void)setsockopt(clientFd, SOL_SOCKET, SO_RCVTIMEO,
|
||||
&rcvTimeout, sizeof(rcvTimeout));
|
||||
#endif
|
||||
|
||||
/* 256 is well below the 2 KB payload, so the highwater check fires partway
|
||||
* through and the ScpStreamRead/ScpStreamSend rekey handling must carry the
|
||||
* transfer to completion. */
|
||||
AssertIntEQ(wolfSSH_SetHighwater(ssh, 256), WS_SUCCESS);
|
||||
|
||||
/* The retry loop only applies to non-blocking. In blocking mode the
|
||||
* ScpStreamRead/ScpStreamSend fixes must carry the rekey transparently, so
|
||||
* a single call completes the transfer; gating on nonBlock keeps the
|
||||
* blocking path from masking a regression that leaves WS_REKEYING set. */
|
||||
tries = 0;
|
||||
do {
|
||||
if (toServer)
|
||||
ret = wolfSSH_SCP_to(ssh, srcBuf, toBuf);
|
||||
else
|
||||
ret = wolfSSH_SCP_from(ssh, srcBuf, fromBuf);
|
||||
err = wolfSSH_get_error(ssh);
|
||||
/* tcp_select() waits for receive-readiness; on WS_WANT_WRITE it has no
|
||||
* write event to wait on, so its 1s timeout is the intended (rare)
|
||||
* fallback that yields the CPU instead of busy-spinning. */
|
||||
if (nonBlock && ret != WS_SUCCESS && (err == WS_WANT_READ
|
||||
|| err == WS_WANT_WRITE || err == WS_REKEYING
|
||||
|| err == WS_CHAN_RXD))
|
||||
tcp_select(clientFd, 1);
|
||||
tries++;
|
||||
} while (nonBlock && ret != WS_SUCCESS && (err == WS_WANT_READ
|
||||
|| err == WS_WANT_WRITE || err == WS_REKEYING
|
||||
|| err == WS_CHAN_RXD)
|
||||
&& tries <= SCP_REKEY_MAX_TRIES);
|
||||
/* Fails fast (instead of hanging CI) if a regression keeps the transfer
|
||||
* stuck in a want/rekey state past the retry bound. */
|
||||
AssertIntLE(tries, SCP_REKEY_MAX_TRIES);
|
||||
AssertIntEQ(ret, WS_SUCCESS);
|
||||
|
||||
/* A mid-transfer rekey must have fired; otherwise the test silently stops
|
||||
* exercising the ScpStreamSend/ScpStreamRead rekey paths it was written
|
||||
* for. */
|
||||
AssertIntGT(kexCount, 0);
|
||||
|
||||
/* best-effort shutdown; the completed transfer above is the real assertion */
|
||||
ret = wolfSSH_shutdown(ssh);
|
||||
(void)ret;
|
||||
|
||||
clientFd = wolfSSH_get_fd(ssh);
|
||||
WCLOSESOCKET(clientFd);
|
||||
wolfSSH_free(ssh);
|
||||
wolfSSH_CTX_free(ctx);
|
||||
ThreadJoin(serThread);
|
||||
|
||||
/* verify the transferred file matches the source once the server is done */
|
||||
AssertIntEQ(scpFilesMatch(verifyName, fileData, SCP_REKEY_FILE_SZ), 0);
|
||||
|
||||
WREMOVE(NULL, srcName);
|
||||
WREMOVE(NULL, verifyName);
|
||||
}
|
||||
|
||||
static void test_wolfSSH_SCP_ReKey(void)
|
||||
{
|
||||
scp_rekey_test(0, 0);
|
||||
}
|
||||
|
||||
static void test_wolfSSH_SCP_ReKey_NonBlock(void)
|
||||
{
|
||||
scp_rekey_test(1, 0);
|
||||
}
|
||||
|
||||
static void test_wolfSSH_SCP_ReKey_ToServer(void)
|
||||
{
|
||||
scp_rekey_test(0, 1);
|
||||
}
|
||||
|
||||
static void test_wolfSSH_SCP_ReKey_ToServer_NonBlock(void)
|
||||
{
|
||||
scp_rekey_test(1, 1);
|
||||
}
|
||||
|
||||
#else /* WOLFSSH_SCP && !NO_WOLFSSH_CLIENT && !SINGLE_THREADED &&
|
||||
* !NO_FILESYSTEM && !WOLFSSH_SCP_USER_CALLBACKS && !WOLFSSH_ZEPHYR */
|
||||
static void test_wolfSSH_SCP_ReKey(void) { ; }
|
||||
static void test_wolfSSH_SCP_ReKey_NonBlock(void) { ; }
|
||||
static void test_wolfSSH_SCP_ReKey_ToServer(void) { ; }
|
||||
static void test_wolfSSH_SCP_ReKey_ToServer_NonBlock(void) { ; }
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef USE_WINDOWS_API
|
||||
static byte color_test[] = {
|
||||
0x1B, 0x5B, 0x34, 0x6D, 0x75, 0x6E, 0x64, 0x65,
|
||||
|
|
@ -3162,6 +3538,10 @@ int wolfSSH_ApiTest(int argc, char** argv)
|
|||
|
||||
/* SCP tests */
|
||||
test_wolfSSH_SCP_CB();
|
||||
test_wolfSSH_SCP_ReKey();
|
||||
test_wolfSSH_SCP_ReKey_NonBlock();
|
||||
test_wolfSSH_SCP_ReKey_ToServer();
|
||||
test_wolfSSH_SCP_ReKey_ToServer_NonBlock();
|
||||
|
||||
/* SFTP tests */
|
||||
test_wolfSSH_SFTP_SendReadPacket();
|
||||
|
|
|
|||
Loading…
Reference in New Issue