mirror of https://github.com/wolfSSL/wolfssh.git
wolfsftp: keep the remote file when resuming a put
- wolfSSH_SFTP_Put() adds WOLFSSH_FXF_TRUNC to the destination open only when the write offset is zero. - STATE_PUT_LOOKUP_OFFSET clears a saved offset when the local file is no larger than it. - A new STATE_PUT_STAT_REMOTE stats the destination when the saved offset is nonzero, and clears the offset unless the reported size matches it exactly, or the stat returns WS_SFTP_STATUS_NOT_OK or WS_PERMISSIONS. Other stat failures re-save the offset and move to STATE_PUT_CLEANUP; a want-read or want-write keeps the state. WS_SFTP_PUT_STATE carries the attributes both states read. - The Windows server open maps WOLFSSH_FXF_CREAT to OPEN_ALWAYS and reserves CREATE_ALWAYS for an open that also asked for WOLFSSH_FXF_TRUNC; the disabled TRUNCATE_EXISTING mapping is dropped. - tests/api.c adds test_wolfSSH_SFTP_PutResume(), five cases over the resume paths, built where the hosted file wrappers are available. Issue: F-11659pull/1171/head
parent
7595a95b37
commit
faca3bc265
|
|
@ -283,6 +283,7 @@ typedef struct WS_SFTP_GET_STATE {
|
|||
enum WS_SFTP_PUT_STATE_ID {
|
||||
STATE_PUT_INIT,
|
||||
STATE_PUT_LOOKUP_OFFSET,
|
||||
STATE_PUT_STAT_REMOTE,
|
||||
STATE_PUT_OPEN_REMOTE,
|
||||
STATE_PUT_OPEN_LOCAL,
|
||||
STATE_PUT_WRITE,
|
||||
|
|
@ -307,6 +308,7 @@ typedef struct WS_SFTP_PUT_STATE {
|
|||
int rSz;
|
||||
byte handle[WOLFSSH_MAX_HANDLE];
|
||||
byte r[WOLFSSH_MAX_SFTP_RW];
|
||||
WS_SFTP_FILEATRB attrib;
|
||||
} WS_SFTP_PUT_STATE;
|
||||
|
||||
|
||||
|
|
@ -2654,11 +2656,13 @@ cleanup:
|
|||
}
|
||||
if (reason & WOLFSSH_FXF_WRITE) {
|
||||
desiredAccess |= GENERIC_WRITE;
|
||||
if (reason & WOLFSSH_FXF_CREAT)
|
||||
creationDisp |= CREATE_ALWAYS;
|
||||
if (reason & WOLFSSH_FXF_CREAT) {
|
||||
if (reason & WOLFSSH_FXF_TRUNC)
|
||||
creationDisp = CREATE_ALWAYS;
|
||||
else
|
||||
creationDisp = OPEN_ALWAYS;
|
||||
}
|
||||
#if 0
|
||||
if (reason & WOLFSSH_FXF_TRUNC)
|
||||
creationDisp |= TRUNCATE_EXISTING;
|
||||
if (reason & WOLFSSH_FXF_EXCL)
|
||||
creationDisp |= CREATE_NEW;
|
||||
if (reason & WOLFSSH_FXF_APPEND)
|
||||
|
|
@ -9988,6 +9992,7 @@ int wolfSSH_SFTP_Put(WOLFSSH* ssh, char* from, char* to, byte resume,
|
|||
WS_STATUS_CB* statusCb)
|
||||
{
|
||||
WS_SFTP_PUT_STATE* state = NULL;
|
||||
word32 openFlags;
|
||||
int ret = WS_SUCCESS;
|
||||
int sz;
|
||||
|
||||
|
|
@ -10029,8 +10034,68 @@ int wolfSSH_SFTP_Put(WOLFSSH* ssh, char* from, char* to, byte resume,
|
|||
if (resume) {
|
||||
/* check if offset was stored */
|
||||
wolfSSH_SFTP_GetOfst(ssh, from, to, state->pOfst);
|
||||
|
||||
/* a saved offset is only usable while the local file
|
||||
* still holds bytes past it */
|
||||
if (state->pOfst[0] > 0 || state->pOfst[1] > 0) {
|
||||
WMEMSET(&state->attrib, 0, sizeof(state->attrib));
|
||||
if (SFTP_GetAttributes(ssh->fs, from, &state->attrib,
|
||||
1, ssh->ctx->heap) == WS_SUCCESS
|
||||
&& (state->attrib.flags
|
||||
& WOLFSSH_FILEATRB_SIZE)) {
|
||||
if (state->attrib.sz[1] < state->pOfst[1]
|
||||
|| (state->attrib.sz[1] == state->pOfst[1]
|
||||
&& state->attrib.sz[0]
|
||||
<= state->pOfst[0])) {
|
||||
WLOG(WS_LOG_SFTP, "Local file too short");
|
||||
state->pOfst[0] = 0;
|
||||
state->pOfst[1] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
state->handleSz = WOLFSSH_MAX_HANDLE;
|
||||
state->state = STATE_PUT_STAT_REMOTE;
|
||||
FALL_THROUGH;
|
||||
|
||||
case STATE_PUT_STAT_REMOTE:
|
||||
WLOG(WS_LOG_SFTP, "SFTP PUT STATE: STAT REMOTE");
|
||||
/* a saved offset is only usable if the destination still
|
||||
* holds exactly that many bytes */
|
||||
if (state->pOfst[0] > 0 || state->pOfst[1] > 0) {
|
||||
ret = wolfSSH_SFTP_STAT(ssh, to, &state->attrib);
|
||||
if (ret != WS_SUCCESS) {
|
||||
if (NoticeError(ssh)) {
|
||||
return WS_FATAL_ERROR;
|
||||
}
|
||||
if (ret != WS_SFTP_STATUS_NOT_OK &&
|
||||
ret != WS_PERMISSIONS) {
|
||||
WLOG(WS_LOG_SFTP, "Error checking remote file");
|
||||
/* put the offset back so a retry can resume */
|
||||
if (wolfSSH_SFTP_SaveOfst(ssh, from, to,
|
||||
state->pOfst) != WS_SUCCESS) {
|
||||
WLOG(WS_LOG_SFTP, "Unable to store offset");
|
||||
}
|
||||
state->state = STATE_PUT_CLEANUP;
|
||||
continue;
|
||||
}
|
||||
/* NOT_OK collapses every status but a permission
|
||||
* failure, so both mean the destination could not be
|
||||
* verified and is safe to overwrite */
|
||||
WLOG(WS_LOG_SFTP, "Remote file unusable, restarting");
|
||||
state->pOfst[0] = 0;
|
||||
state->pOfst[1] = 0;
|
||||
ssh->error = WS_SUCCESS;
|
||||
ret = WS_SUCCESS;
|
||||
}
|
||||
else if ((state->attrib.flags & WOLFSSH_FILEATRB_SIZE) == 0
|
||||
|| state->attrib.sz[0] != state->pOfst[0]
|
||||
|| state->attrib.sz[1] != state->pOfst[1]) {
|
||||
WLOG(WS_LOG_SFTP, "Size does not match, starting over");
|
||||
state->pOfst[0] = 0;
|
||||
state->pOfst[1] = 0;
|
||||
}
|
||||
}
|
||||
state->state = STATE_PUT_OPEN_LOCAL;
|
||||
FALL_THROUGH;
|
||||
|
||||
|
|
@ -10104,9 +10169,14 @@ int wolfSSH_SFTP_Put(WOLFSSH* ssh, char* from, char* to, byte resume,
|
|||
|
||||
case STATE_PUT_OPEN_REMOTE:
|
||||
WLOG(WS_LOG_SFTP, "SFTP PUT STATE: OPEN REMOTE");
|
||||
/* only truncate when starting from the beginning of the file */
|
||||
openFlags = WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT;
|
||||
if (state->pOfst[0] == 0 && state->pOfst[1] == 0) {
|
||||
openFlags |= WOLFSSH_FXF_TRUNC;
|
||||
}
|
||||
|
||||
/* open file and get handle */
|
||||
ret = wolfSSH_SFTP_Open(ssh, to, (WOLFSSH_FXF_WRITE |
|
||||
WOLFSSH_FXF_CREAT | WOLFSSH_FXF_TRUNC), NULL,
|
||||
ret = wolfSSH_SFTP_Open(ssh, to, openFlags, NULL,
|
||||
state->handle, &state->handleSz);
|
||||
if (ret != WS_SUCCESS) {
|
||||
if (ssh->error == WS_WANT_READ ||
|
||||
|
|
|
|||
266
tests/api.c
266
tests/api.c
|
|
@ -5680,6 +5680,270 @@ static void test_wolfSSH_SFTP_SaveOfst(void)
|
|||
wolfSSH_CTX_free(ctx);
|
||||
}
|
||||
|
||||
|
||||
#if !defined(NO_FILESYSTEM) && !defined(WOLFSSH_USER_FILESYSTEM) && \
|
||||
!defined(WOLFSSH_ZEPHYR)
|
||||
|
||||
#define SFTP_PUT_RESUME_SZ 1024
|
||||
#define SFTP_PUT_RESUME_OFST 256
|
||||
|
||||
/* Fills buf with the source file's byte pattern. */
|
||||
static void sftpPutFillPattern(byte* buf, word32 sz)
|
||||
{
|
||||
word32 i;
|
||||
|
||||
for (i = 0; i < sz; i++) {
|
||||
buf[i] = (byte)(i * 7 + 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Writes sz bytes of buf to the local file name. Returns 0 on success. */
|
||||
static int sftpPutWriteFile(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 name holds exactly sz bytes equal to expect. Asking for one
|
||||
* byte more than expected also catches a file left too long. */
|
||||
static int sftpPutFileMatches(const char* name, const byte* expect, word32 sz)
|
||||
{
|
||||
WFILE* fp = NULL;
|
||||
byte got[SFTP_PUT_RESUME_SZ + 1];
|
||||
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 + 1, fp) != sz)
|
||||
ret = -1;
|
||||
|
||||
if (ret == 0 && WMEMCMP(got, expect, sz) != 0)
|
||||
ret = -1;
|
||||
|
||||
WFCLOSE(NULL, fp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/* Leaves sz bytes of buf in the remote file name, the way an interrupted
|
||||
* upload would. Returns WS_SUCCESS on success. */
|
||||
static int sftpPutStageRemote(WOLFSSH* ssh, char* name, byte* buf, word32 sz)
|
||||
{
|
||||
byte handle[WOLFSSH_MAX_HANDLE];
|
||||
word32 handleSz;
|
||||
word32 wrote = 0;
|
||||
word32 ofst[2];
|
||||
int ret = WS_FATAL_ERROR;
|
||||
int tries;
|
||||
int sent;
|
||||
|
||||
for (tries = 0; tries < SFTP_MAX_RETRY_TRIES; tries++) {
|
||||
handleSz = WOLFSSH_MAX_HANDLE;
|
||||
ret = wolfSSH_SFTP_Open(ssh, name,
|
||||
WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT | WOLFSSH_FXF_TRUNC,
|
||||
NULL, handle, &handleSz);
|
||||
if (ret == WS_SUCCESS ||
|
||||
!sftp_error_keeps_state(wolfSSH_get_error(ssh))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ret != WS_SUCCESS) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
for (tries = 0; tries < SFTP_MAX_RETRY_TRIES && wrote < sz; tries++) {
|
||||
ofst[0] = wrote;
|
||||
ofst[1] = 0;
|
||||
sent = wolfSSH_SFTP_SendWritePacket(ssh, handle, handleSz, ofst,
|
||||
buf + wrote, sz - wrote);
|
||||
if (sent > 0) {
|
||||
wrote += (word32)sent;
|
||||
continue;
|
||||
}
|
||||
if (!sftp_error_keeps_state(wolfSSH_get_error(ssh))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ret = sftp_retry_close(ssh, handle, handleSz);
|
||||
if (ret == WS_SUCCESS && wrote != sz) {
|
||||
ret = WS_FATAL_ERROR;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/* Drives one wolfSSH_SFTP_Put() to a terminal result. */
|
||||
static int sftpPutToCompletion(WOLFSSH* ssh, char* from, char* to, byte resume)
|
||||
{
|
||||
int ret = WS_FATAL_ERROR;
|
||||
int tries;
|
||||
|
||||
for (tries = 0; tries < SFTP_MAX_RETRY_TRIES; tries++) {
|
||||
ret = wolfSSH_SFTP_Put(ssh, from, to, resume, NULL);
|
||||
if (ret == WS_SUCCESS ||
|
||||
!sftp_error_is_notice(wolfSSH_get_error(ssh))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
#endif /* !NO_FILESYSTEM && !WOLFSSH_USER_FILESYSTEM && !WOLFSSH_ZEPHYR */
|
||||
|
||||
|
||||
/* A resumed put must keep the bytes already at the destination, and must
|
||||
* only resume onto a destination whose size matches the saved offset. The
|
||||
* echoserver shares this process, so the "remote" file is read locally. */
|
||||
static void test_wolfSSH_SFTP_PutResume(void)
|
||||
{
|
||||
/* staging the local source file needs the hosted fopen()/getcwd() wrappers */
|
||||
#if !defined(NO_FILESYSTEM) && !defined(WOLFSSH_USER_FILESYSTEM) && \
|
||||
!defined(WOLFSSH_ZEPHYR)
|
||||
func_args ser;
|
||||
tcp_ready ready;
|
||||
int argsCount;
|
||||
WS_SOCKET_T clientFd;
|
||||
|
||||
const char* args[10];
|
||||
WOLFSSH_CTX* ctx = NULL;
|
||||
WOLFSSH* ssh = NULL;
|
||||
|
||||
THREAD_TYPE serThread;
|
||||
|
||||
byte src[SFTP_PUT_RESUME_SZ];
|
||||
byte stale[SFTP_PUT_RESUME_SZ + SFTP_PUT_RESUME_OFST];
|
||||
byte expect[SFTP_PUT_RESUME_SZ];
|
||||
word32 ofst[2];
|
||||
char srcName[] = "wolfssh_11659_src.tmp";
|
||||
char dstName[] = "wolfssh_11659_dst.tmp";
|
||||
|
||||
WMEMSET(&ser, 0, sizeof(func_args));
|
||||
|
||||
argsCount = 0;
|
||||
args[argsCount++] = ".";
|
||||
args[argsCount++] = "-1";
|
||||
args[argsCount++] = "-p";
|
||||
args[argsCount++] = "0";
|
||||
ser.argv = (char**)args;
|
||||
ser.argc = argsCount;
|
||||
ser.signal = &ready;
|
||||
InitTcpReady(ser.signal);
|
||||
ThreadStart(echoserver_test, (void*)&ser, &serThread);
|
||||
WaitTcpReady(&ready);
|
||||
|
||||
sftp_client_connect(&ctx, &ssh, ready.port);
|
||||
AssertNotNull(ctx);
|
||||
AssertNotNull(ssh);
|
||||
|
||||
sftpPutFillPattern(src, (word32)sizeof(src));
|
||||
WMEMSET(stale, 0xFF, sizeof(stale));
|
||||
AssertIntEQ(sftpPutWriteFile(srcName, src, (word32)sizeof(src)), 0);
|
||||
|
||||
/* the saved offset matches the destination, so the upload picks up where
|
||||
* it left off. Staging a prefix unlike the source, and expecting it back
|
||||
* untouched, is what separates a resume from a full re-upload. */
|
||||
(void)sftp_retry_remove(ssh, dstName); /* normally absent */
|
||||
AssertIntEQ(sftpPutStageRemote(ssh, dstName, stale, SFTP_PUT_RESUME_OFST),
|
||||
WS_SUCCESS);
|
||||
WMEMCPY(expect, stale, SFTP_PUT_RESUME_OFST);
|
||||
WMEMCPY(expect + SFTP_PUT_RESUME_OFST, src + SFTP_PUT_RESUME_OFST,
|
||||
sizeof(expect) - SFTP_PUT_RESUME_OFST);
|
||||
ofst[0] = SFTP_PUT_RESUME_OFST;
|
||||
ofst[1] = 0;
|
||||
AssertIntEQ(wolfSSH_SFTP_SaveOfst(ssh, srcName, dstName, ofst),
|
||||
WS_SUCCESS);
|
||||
AssertIntEQ(sftpPutToCompletion(ssh, srcName, dstName, 1), WS_SUCCESS);
|
||||
AssertIntEQ(sftpPutFileMatches(dstName, expect, (word32)sizeof(expect)), 0);
|
||||
|
||||
/* the destination is gone, so the whole file is sent again */
|
||||
AssertIntEQ(sftp_retry_remove(ssh, dstName), WS_SUCCESS);
|
||||
ofst[0] = SFTP_PUT_RESUME_OFST;
|
||||
ofst[1] = 0;
|
||||
AssertIntEQ(wolfSSH_SFTP_SaveOfst(ssh, srcName, dstName, ofst),
|
||||
WS_SUCCESS);
|
||||
AssertIntEQ(sftpPutToCompletion(ssh, srcName, dstName, 1), WS_SUCCESS);
|
||||
AssertIntEQ(sftpPutFileMatches(dstName, src, (word32)sizeof(src)), 0);
|
||||
|
||||
/* the destination is longer than the saved offset, so the whole file is
|
||||
* sent again; staging bytes unlike the source makes a resume here show
|
||||
* up in the result */
|
||||
AssertIntEQ(sftp_retry_remove(ssh, dstName), WS_SUCCESS);
|
||||
AssertIntEQ(sftpPutStageRemote(ssh, dstName, stale, SFTP_PUT_RESUME_OFST),
|
||||
WS_SUCCESS);
|
||||
ofst[0] = SFTP_PUT_RESUME_OFST / 2;
|
||||
ofst[1] = 0;
|
||||
AssertIntEQ(wolfSSH_SFTP_SaveOfst(ssh, srcName, dstName, ofst),
|
||||
WS_SUCCESS);
|
||||
AssertIntEQ(sftpPutToCompletion(ssh, srcName, dstName, 1), WS_SUCCESS);
|
||||
AssertIntEQ(sftpPutFileMatches(dstName, src, (word32)sizeof(src)), 0);
|
||||
|
||||
/* a plain put still truncates a longer destination */
|
||||
AssertIntEQ(sftp_retry_remove(ssh, dstName), WS_SUCCESS);
|
||||
AssertIntEQ(sftpPutStageRemote(ssh, dstName, stale, (word32)sizeof(stale)),
|
||||
WS_SUCCESS);
|
||||
AssertIntEQ(sftpPutToCompletion(ssh, srcName, dstName, 0), WS_SUCCESS);
|
||||
AssertIntEQ(sftpPutFileMatches(dstName, src, (word32)sizeof(src)), 0);
|
||||
|
||||
/* the source has nothing left past the saved offset, so the whole file
|
||||
* is sent again. This case shortens the source, so it runs last. */
|
||||
AssertIntEQ(sftp_retry_remove(ssh, dstName), WS_SUCCESS);
|
||||
AssertIntEQ(sftpPutStageRemote(ssh, dstName, stale, SFTP_PUT_RESUME_OFST),
|
||||
WS_SUCCESS);
|
||||
AssertIntEQ(sftpPutWriteFile(srcName, src, SFTP_PUT_RESUME_OFST / 2), 0);
|
||||
ofst[0] = SFTP_PUT_RESUME_OFST;
|
||||
ofst[1] = 0;
|
||||
AssertIntEQ(wolfSSH_SFTP_SaveOfst(ssh, srcName, dstName, ofst),
|
||||
WS_SUCCESS);
|
||||
AssertIntEQ(sftpPutToCompletion(ssh, srcName, dstName, 1), WS_SUCCESS);
|
||||
AssertIntEQ(sftpPutFileMatches(dstName, src, SFTP_PUT_RESUME_OFST / 2), 0);
|
||||
|
||||
AssertIntEQ(sftp_retry_remove(ssh, dstName), WS_SUCCESS);
|
||||
WREMOVE(NULL, srcName);
|
||||
|
||||
/* take care of re-keying state before shutdown call */
|
||||
while (wolfSSH_get_error(ssh) == WS_REKEYING) {
|
||||
wolfSSH_worker(ssh, NULL);
|
||||
}
|
||||
|
||||
argsCount = AbsorbBenignReset(ssh, wolfSSH_shutdown(ssh));
|
||||
#if DEFAULT_HIGHWATER_MARK < 8000
|
||||
if (argsCount == WS_REKEYING) {
|
||||
argsCount = WS_SUCCESS;
|
||||
}
|
||||
#endif
|
||||
AssertIntEQ(argsCount, WS_SUCCESS);
|
||||
|
||||
clientFd = wolfSSH_get_fd(ssh);
|
||||
WCLOSESOCKET(clientFd);
|
||||
|
||||
wolfSSH_free(ssh);
|
||||
wolfSSH_CTX_free(ctx);
|
||||
#ifdef WOLFSSH_ZEPHYR
|
||||
k_sleep(Z_TIMEOUT_TICKS(100));
|
||||
#endif
|
||||
ThreadJoin(serThread);
|
||||
FreeTcpReady(&ready);
|
||||
#endif /* !NO_FILESYSTEM && !WOLFSSH_USER_FILESYSTEM && !WOLFSSH_ZEPHYR */
|
||||
}
|
||||
|
||||
|
||||
#else /* WOLFSSH_SFTP && !NO_WOLFSSH_CLIENT && !SINGLE_THREADED */
|
||||
static void test_wolfSSH_SFTP_SendReadPacket(void) { ; }
|
||||
static void test_wolfSSH_SFTP_PartialSend(void) { ; }
|
||||
|
|
@ -5690,6 +5954,7 @@ static void test_wolfSSH_SFTP_StartPathNotConfined(void) { ; }
|
|||
static void test_wolfSSH_SFTP_SetConfinePath(void) { ; }
|
||||
static void test_wolfSSH_SFTP_SetDefaultPath(void) { ; }
|
||||
static void test_wolfSSH_SFTP_SaveOfst(void) { ; }
|
||||
static void test_wolfSSH_SFTP_PutResume(void) { ; }
|
||||
#endif /* WOLFSSH_SFTP && !NO_WOLFSSH_CLIENT && !SINGLE_THREADED */
|
||||
|
||||
|
||||
|
|
@ -7555,6 +7820,7 @@ int wolfSSH_ApiTest(int argc, char** argv)
|
|||
test_wolfSSH_SFTP_SetConfinePath();
|
||||
test_wolfSSH_SFTP_SetDefaultPath();
|
||||
test_wolfSSH_SFTP_SaveOfst();
|
||||
test_wolfSSH_SFTP_PutResume();
|
||||
|
||||
/* Either SCP or SFTP */
|
||||
test_wolfSSH_RealPath();
|
||||
|
|
|
|||
Loading…
Reference in New Issue