diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 435cec473..732d4e8b4 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -86,6 +86,7 @@ #endif #ifndef USE_WINDOWS_API #include + #include #endif #include #if defined(__QNX__) || defined(__QNXNTO__) @@ -139,6 +140,15 @@ static int quit = 0; wolfSSL_Mutex doneLock; #define MAX_PASSWD_RETRY 3 static int passwdRetry = MAX_PASSWD_RETRY; +/* ssh_worker() reads ChildRunning whether or not a shell is compiled in. + * With a shell, ChildSig() writes it from a SIGCHLD handler, so it has to + * be sig_atomic_t; without one there is no handler, and no signal.h to + * declare that type. Zephyr's libc has neither. */ +#ifdef WOLFSSH_SHELL +static volatile sig_atomic_t ChildRunning = 0; +#else +static volatile int ChildRunning = 0; +#endif #ifndef EXAMPLE_HIGHWATER_MARK @@ -197,7 +207,7 @@ typedef struct WS_FwdCbActionCtx { typedef struct { WOLFSSH* ssh; WS_SOCKET_T fd; - word32 id; + word32 tid; int echo; char nonBlock; #if defined(WOLFSSL_PTHREADS) && defined(WOLFSSL_TEST_GLOBAL_REQ) @@ -212,6 +222,12 @@ typedef struct { WS_FwdCbActionCtx fwdCbCtx; #endif WS_AppCtx shellCtx; +#ifdef WOLFSSH_SFTP + int doSftp; +#endif +#ifdef WOLFSSH_SCP + int doScp; +#endif byte channelBuffer[EXAMPLE_BUFFER_SZ]; /* The EOF drain holds an unsent tail across worker passes, * so it cannot share channelBuffer with the read path. */ @@ -250,7 +266,7 @@ static int dump_stats(thread_ctx_t* ctx) "Statistics for Thread #%u:\r\n" " txCount = %u\r\n rxCount = %u\r\n" " seq = %u\r\n peerSeq = %u\r\n", - ctx->id, txCount, rxCount, seq, peerSeq); + ctx->tid, txCount, rxCount, seq, peerSeq); statsSz = (word32)WSTRLEN(ctx->statsBuffer); fprintf(stderr, "%s", ctx->statsBuffer); @@ -659,8 +675,9 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx, else if (action == WOLFSSH_FWD_CHANNEL_ID) { appCtx->channelId = port; } - else + else { ret = WS_FWD_INVALID_ACTION; + } return ret; } @@ -668,6 +685,277 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx, #endif /* WOLFSSH_FWD */ +#ifdef WOLFSSH_SHELL +static void ChildSig(int sig) +{ + (void)sig; + ChildRunning = 0; +} + + +/* Undo a forkpty() whose session request is about to be refused: the shell + * would run on behind a pty nothing reads, and its exit would clear + * ChildRunning out from under the worker loop. */ +static void ShellChildCleanup(thread_ctx_t* threadCtx, pid_t childPid) +{ + void (*prevSig)(int); + + if (threadCtx->shellCtx.appFd >= 0) { + WCLOSESOCKET(threadCtx->shellCtx.appFd); + threadCtx->shellCtx.appFd = -1; + } + + /* This exit is ours, not the session's. */ + prevSig = signal(SIGCHLD, SIG_DFL); + kill(childPid, SIGKILL); + waitpid(childPid, NULL, 0); + signal(SIGCHLD, prevSig); +} + + +#ifdef SHELL_DEBUG +static int termios_show(int fd) +{ + struct termios tios; + int i; + int rc; + + WMEMSET((void *) &tios, 0, sizeof(tios)); + rc = tcgetattr(fd, &tios); + printf("tcgetattr returns=%x\n", rc); + + printf("iflag/oflag/cflag/lflag = %x/%x/%x/%x\n", + (unsigned int)tios.c_iflag, (unsigned int)tios.c_oflag, + (unsigned int)tios.c_cflag, (unsigned int)tios.c_lflag); + printf("c_ispeed/c_ospeed = %x/%x\n", + (unsigned int)tios.c_ispeed, (unsigned int)tios.c_ospeed); + for (i = 0; i < NCCS; i++) { + printf("c_cc[%d] = %hhx\n", i, tios.c_cc[i]); + } + return 0; +} +#endif +#endif /* WOLFSSH_SHELL */ + + +/* One program start per connection, as RFC 4254 section 6.5 allows. A + * second start would fork a shell over the running one, or hand the + * session to sftp or scp, whose divert closes the pty. */ +static int SessionInUse(const thread_ctx_t* threadCtx) +{ + int inUse; + + /* WS_SOCKET_T is unsigned on Windows, so the unset fd is -1 rather + * than anything below zero. */ + inUse = threadCtx->shellCtx.state == APP_STATE_CONNECTED + || threadCtx->shellCtx.appFd != (WS_SOCKET_T)-1; +#ifdef WOLFSSH_SFTP + inUse = inUse || threadCtx->doSftp; +#endif +#ifdef WOLFSSH_SCP + inUse = inUse || threadCtx->doScp; +#endif + + return inUse; +} + + +/* Registered in every build, in both modes: with no shell the echoserver + * still has to take the channel to mark it connected, so ssh_worker() will + * echo on it. Returns WS_SUCCESS to accept the request, 1 to reject it. */ +static int wsShellStartCb(WOLFSSH_CHANNEL* channel, void* ctx) +{ + thread_ctx_t* threadCtx = (thread_ctx_t*)ctx; + word32 channelId = 0; + + if (threadCtx == NULL) { + return 1; + } + + if (SessionInUse(threadCtx)) { + return 1; + } + + /* Our own id: it is what wolfSSH_worker() reports and what the read, + * send, and find calls below take. */ + if (wolfSSH_ChannelGetId(channel, &channelId, WS_CHANNEL_ID_SELF) + != WS_SUCCESS) { + return 1; + } + +#ifdef WOLFSSH_SHELL + /* Echo mode has no shell to start, ssh_worker() echoes the channel data + * back through the SSH stream. */ + if (!threadCtx->echo) { + WOLFSSH* ssh; + const char *userName; + struct passwd *p_passwd; + struct termios tios; + pid_t childPid; + int rc; + + ssh = threadCtx->ssh; + userName = wolfSSH_GetUsername(ssh); + p_passwd = getpwnam((const char *)userName); + if (p_passwd == NULL) { + /* Not actually a user on the system. */ + #ifdef SHELL_DEBUG + fprintf(stderr, "user %s does not exist\n", userName); + #endif + return 1; + } + + childPid = forkpty(&threadCtx->shellCtx.appFd, NULL, NULL, NULL); + + if (childPid < 0) { + /* Refuse the request; the connection carries on without it. */ + return 1; + } + else if (childPid == 0) { + /* Child process */ + const char *args[] = {"-sh", NULL}; + + signal(SIGINT, SIG_DFL); + + #ifdef SHELL_DEBUG + printf("userName is %s\n", userName); + system("env"); + #endif + + setenv("HOME", p_passwd->pw_dir, 1); + setenv("LOGNAME", p_passwd->pw_name, 1); + rc = chdir(p_passwd->pw_dir); + if (rc != 0) { + /* Never return: the child would run on inside the library + * and write to the parent's socket. */ + _exit(EXIT_FAILURE); + } + + execv("/bin/sh", (char **)args); + _exit(EXIT_FAILURE); + } + #ifdef SHELL_DEBUG + printf("In childPid > 0; getpid=%d\n", (int)getpid()); + #endif + rc = tcgetattr(threadCtx->shellCtx.appFd, &tios); + if (rc != 0) { + printf("tcgetattr failed: rc =%d,errno=%x\n", rc, errno); + ShellChildCleanup(threadCtx, childPid); + return 1; + } + rc = tcsetattr(threadCtx->shellCtx.appFd, TCSAFLUSH, &tios); + if (rc != 0) { + printf("tcsetattr failed: rc =%d,errno=%x\n", rc, errno); + ShellChildCleanup(threadCtx, childPid); + return 1; + } + + /* Installed only now: the refusals above reap their own child. */ + signal(SIGCHLD, ChildSig); + + #ifdef SHELL_DEBUG + termios_show(threadCtx->shellCtx.appFd); + #endif + + /* set initial size of terminal based on saved size */ + #if !defined(NO_TERMIOS) && defined(WOLFSSH_TERM) + #if defined(HAVE_SYS_IOCTL_H) + wolfSSH_DoModes(ssh->modes, ssh->modesSz, threadCtx->shellCtx.appFd); + { + struct winsize s = {0}; + + s.ws_col = ssh->widthChar; + s.ws_row = ssh->heightRows; + s.ws_xpixel = ssh->widthPixels; + s.ws_ypixel = ssh->heightPixels; + + ioctl(threadCtx->shellCtx.appFd, TIOCSWINSZ, &s); + } + #endif /* HAVE_SYS_IOCTL_H */ + + wolfSSH_SetTerminalResizeCtx(ssh, (void*)&threadCtx->shellCtx.appFd); + #endif /* !NO_TERMIOS && WOLFSSH_TERM */ + } +#endif /* WOLFSSH_SHELL */ + + /* Claim the channel only once it can be served. Claiming it up front + * would leave the worker driving a connected shell that never started. */ + threadCtx->shellCtx.channelId = channelId; + threadCtx->shellCtx.state = APP_STATE_CONNECTED; + + return WS_SUCCESS; +} + + +#ifdef WOLFSSH_SFTP +static int wsSubsysStartCb(WOLFSSH_CHANNEL* channel, void* vCtx) +{ + int rej = 1; + + if (vCtx && channel) { + thread_ctx_t* threadCtx; + const char* cmd; + WS_SessionType type; + + threadCtx = (thread_ctx_t*)vCtx; + + if (SessionInUse(threadCtx)) { + return 1; + } + + cmd = wolfSSH_ChannelGetSessionCommand(channel); + type = wolfSSH_ChannelGetSessionType(channel); + + /* A truncated subsystem string leaves the command NULL, and this + * runs before anything else has looked at it. The name matches + * whole, length and bytes, as wolfSSH_SFTP_accept() asks: granting + * sftp with an embedded NUL answers success on a session it then + * refuses. */ + if (type == WOLFSSH_SESSION_SUBSYSTEM && cmd != NULL + && wolfSSH_ChannelGetSessionCommandSz(channel) + == (word32)WSTRLEN("sftp") + && WSTRCMP(cmd, "sftp") == 0) { + threadCtx->doSftp = 1; + rej = WS_SUCCESS; + } + } + + return rej; +} +#endif /* WOLFSSH_SFTP */ + + +/* An "scp ..." command starts a transfer, anything else runs as a session, + * the same as a shell request: the echoserver never runs the command. */ +static int wsExecStartCb(WOLFSSH_CHANNEL* channel, void* vCtx) +{ + int rej = 1; + + if (vCtx && channel) { + const char* cmd = wolfSSH_ChannelGetSessionCommand(channel); + + if (SessionInUse((thread_ctx_t*)vCtx)) { + return 1; + } + +#ifdef WOLFSSH_SCP + /* The prefix ChannelCommandIsScp() matches, so both modes agree. */ + if (cmd != NULL && WSTRNCMP(cmd, "scp", 3) == 0) { + ((thread_ctx_t*)vCtx)->doScp = 1; + rej = WS_SUCCESS; + } + else +#endif /* WOLFSSH_SCP */ + { + rej = wsShellStartCb(channel, vCtx); + } + (void)cmd; + } + + return rej; +} + + #ifdef SHELL_DEBUG static void display_ascii(char *p_buf, @@ -709,30 +997,6 @@ static void buf_dump(unsigned char *buf, int len) return; } - -#ifdef WOLFSSH_SHELL -static int termios_show(int fd) -{ - struct termios tios; - int i; - int rc; - - WMEMSET((void *) &tios, 0, sizeof(tios)); - rc = tcgetattr(fd, &tios); - printf("tcgetattr returns=%x\n", rc); - - printf("iflag/oflag/cflag/lflag = %x/%x/%x/%x\n", - (unsigned int)tios.c_iflag, (unsigned int)tios.c_oflag, - (unsigned int)tios.c_cflag, (unsigned int)tios.c_lflag); - printf("c_ispeed/c_ospeed = %x/%x\n", - (unsigned int)tios.c_ispeed, (unsigned int)tios.c_ospeed); - for (i = 0; i < NCCS; i++) { - printf("c_cc[%d] = %hhx\n", i, tios.c_cc[i]); - } - return 0; -} -#endif /* WOLFSSH_SHELL */ - #endif /* SHELL_DEBUG */ @@ -817,21 +1081,14 @@ static int termios_show(int fd) #endif -int ChildRunning = 0; - -#ifdef WOLFSSH_SHELL -static void ChildSig(int sig) -{ - (void)sig; - ChildRunning = 0; -} -#endif - static int ssh_worker(thread_ctx_t* threadCtx) { WOLFSSH* ssh; WS_SOCKET_T sshFd; int rc = 0; + /* What the loop hands back, so a transfer taking over the session + * still leaves through the cleanup below it. */ + int workerRet = 0; int eofAnswered = 0; /* Held across passes with 0 <= eofOff <= eofRead. */ int eofRead = 0; @@ -839,11 +1096,8 @@ static int ssh_worker(thread_ctx_t* threadCtx) /* 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; - WS_SOCKET_T childFd = 0; - pid_t childPid; +#ifdef WOLFSSH_AGENT + int agentOpened = 0; #endif #if defined(WOLFSSL_PTHREADS) && defined(WOLFSSL_TEST_GLOBAL_REQ) pthread_t globalReq_th; @@ -862,6 +1116,29 @@ static int ssh_worker(thread_ctx_t* threadCtx) sshFd = wolfSSH_get_fd(ssh); + if (threadCtx->shellCtx.state != APP_STATE_CONNECTED) { + /* The legacy path: accept() answered the session request itself, + * with no callback registered to claim the channel. Take it when + * it was granted and there is somewhere to put the data: nothing + * started a shell, so a shell build serves it only in echo mode. + * The grant is read from internal.h; the library has no public + * accessor for it yet. */ + WOLFSSH_CHANNEL* sessionChannel; + int canServe = 1; + +#ifdef WOLFSSH_SHELL + canServe = echoOnly || threadCtx->shellCtx.appFd >= 0; +#endif + + sessionChannel = wolfSSH_ChannelNext(ssh, NULL); + if (canServe && sessionChannel != NULL + && sessionChannel->sessionGranted) { + threadCtx->shellCtx.state = APP_STATE_CONNECTED; + wolfSSH_ChannelGetId(sessionChannel, + &threadCtx->shellCtx.channelId, WS_CHANNEL_ID_SELF); + } + } + #if defined(WOLFSSL_PTHREADS) && defined(WOLFSSL_TEST_GLOBAL_REQ) /* submit Global Request for keep-alive */ rc = pthread_create(&globalReq_th, NULL, global_req, threadCtx); @@ -869,57 +1146,11 @@ static int ssh_worker(thread_ctx_t* threadCtx) printf("pthread_create() failed.\n"); #endif -#ifdef WOLFSSH_SHELL - if (!threadCtx->echo) { - - userName = wolfSSH_GetUsername(ssh); - p_passwd = getpwnam((const char *)userName); - if (p_passwd == NULL) { - /* Not actually a user on the system. */ - #ifdef SHELL_DEBUG - fprintf(stderr, "user %s does not exist\n", userName); - #endif - return WS_FATAL_ERROR; - } - - ChildRunning = 1; - childPid = forkpty(&childFd, NULL, NULL, NULL); - - if (childPid < 0) { - /* forkpty failed, so return */ - ChildRunning = 0; - return WS_FATAL_ERROR; - } - else if (childPid == 0) { - /* Child process */ - const char *args[] = {"-sh", NULL}; - - signal(SIGINT, SIG_DFL); - - #ifdef SHELL_DEBUG - printf("userName is %s\n", userName); - system("env"); - #endif - - setenv("HOME", p_passwd->pw_dir, 1); - setenv("LOGNAME", p_passwd->pw_name, 1); - rc = chdir(p_passwd->pw_dir); - if (rc != 0) { - return WS_FATAL_ERROR; - } - - execv("/bin/sh", (char **)args); - } - } -#endif { /* Parent process */ -#ifdef WOLFSSH_SHELL - struct termios tios; -#endif + int wantWrite = 0; #ifdef WOLFSSH_AGENT WS_SOCKET_T agentFd = -1; - WS_SOCKET_T agentListenFd = threadCtx->agentCtx.listenFd; word32 agentChannelId = -1; #endif #ifdef WOLFSSH_FWD @@ -927,55 +1158,12 @@ static int ssh_worker(thread_ctx_t* threadCtx) word32 fwdBufferIdx = 0; #endif -#ifdef WOLFSSH_SHELL - if (!threadCtx->echo) { - #ifdef SHELL_DEBUG - printf("In childPid > 0; getpid=%d\n", (int)getpid()); - #endif - signal(SIGCHLD, ChildSig); - - rc = tcgetattr(childFd, &tios); - if (rc != 0) { - printf("tcgetattr failed: rc =%d,errno=%x\n", rc, errno); - return WS_FATAL_ERROR; - } - rc = tcsetattr(childFd, TCSAFLUSH, &tios); - if (rc != 0) { - printf("tcsetattr failed: rc =%d,errno=%x\n", rc, errno); - return WS_FATAL_ERROR; - } - - #ifdef SHELL_DEBUG - termios_show(childFd); - #endif - } - else - ChildRunning = 1; -#else ChildRunning = 1; -#endif - -#if !defined(NO_TERMIOS) && defined(WOLFSSH_TERM) && defined(WOLFSSH_SHELL) -#if defined(HAVE_SYS_IOCTL_H) - /* if not echoing, set initial size of terminal based on saved size */ - if (!threadCtx->echo) { - struct winsize s = {0,0,0,0}; - - wolfSSH_DoModes(ssh->modes, ssh->modesSz, childFd); - s.ws_col = ssh->widthChar; - s.ws_row = ssh->heightRows; - s.ws_xpixel = ssh->widthPixels; - s.ws_ypixel = ssh->heightPixels; - - ioctl(childFd, TIOCSWINSZ, &s); - - wolfSSH_SetTerminalResizeCtx(ssh, (void*)&childFd); - } -#endif /* HAVE_SYS_IOCTL_H */ -#endif /* !NO_TERMIOS && WOLFSSH_TERM && WOLFSSH_SHELL */ while (ChildRunning) { fd_set readFds; + fd_set writeFds; + int writable; WS_SOCKET_T maxFd; int cnt_r; int cnt_w; @@ -984,18 +1172,37 @@ static int ssh_worker(thread_ctx_t* threadCtx) FD_SET(sshFd, &readFds); maxFd = sshFd; + FD_ZERO(&writeFds); + if (wantWrite) + FD_SET(sshFd, &writeFds); + + #ifdef WOLFSSH_AGENT + /* The peer's auth-agent-req lands after wolfSSH_accept() has + * already returned in application-driven mode, so the channel + * answering it is opened here rather than inside accept(). The + * call reports WS_BAD_ARGUMENT until the request arrives. */ + if (!agentOpened + && wolfSSH_AGENT_ChannelOpen(ssh) == WS_SUCCESS) { + agentOpened = 1; + } + #endif + #ifdef WOLFSSH_SHELL - if (!threadCtx->echo) { - FD_SET(childFd, &readFds); - if (childFd > maxFd) - maxFd = childFd; + if (threadCtx->shellCtx.state == APP_STATE_CONNECTED + && threadCtx->shellCtx.appFd >= 0) { + FD_SET(threadCtx->shellCtx.appFd, &readFds); + if (threadCtx->shellCtx.appFd > maxFd) + maxFd = threadCtx->shellCtx.appFd; } #endif /* WOLFSSH_SHELL */ #ifdef WOLFSSH_AGENT - if (threadCtx->agentCtx.state == APP_STATE_LISTEN) { - FD_SET(agentListenFd, &readFds); - if (agentListenFd > maxFd) - maxFd = agentListenFd; + /* The poll above creates this listener mid-loop; re-read it + * each pass rather than caching it. */ + if (threadCtx->agentCtx.state == APP_STATE_LISTEN + && threadCtx->agentCtx.listenFd >= 0) { + FD_SET(threadCtx->agentCtx.listenFd, &readFds); + if (threadCtx->agentCtx.listenFd > maxFd) + maxFd = threadCtx->agentCtx.listenFd; } if (agentFd >= 0 && threadCtx->agentCtx.state == APP_STATE_CONNECTED) { @@ -1020,12 +1227,16 @@ static int ssh_worker(thread_ctx_t* threadCtx) maxFd = fwdFd; } #endif /* WOLFSSH_FWD */ - rc = select((int)maxFd + 1, &readFds, NULL, NULL, NULL); + + rc = select((int)maxFd + 1, &readFds, + wantWrite ? &writeFds : NULL, NULL, NULL); if (rc == -1) { break; } + writable = wantWrite && FD_ISSET(sshFd, &writeFds); + wantWrite = 0; - if (FD_ISSET(sshFd, &readFds)) { + if (FD_ISSET(sshFd, &readFds) || writable) { word32 lastChannel = 0; /* The following tries to read from the first channel inside @@ -1035,6 +1246,18 @@ static int ssh_worker(thread_ctx_t* threadCtx) channel. The additional channel is only used with the agent. */ cnt_r = wolfSSH_worker(ssh, &lastChannel); + #ifdef WOLFSSH_SFTP + if (threadCtx->doSftp) { + workerRet = WS_SFTP_COMPLETE; + break; + } + #endif + #ifdef WOLFSSH_SCP + if (threadCtx->doScp) { + workerRet = WS_SCP_INIT; + break; + } + #endif /* Take the worker's status before the drain below: its * reads and sends latch their own into ssh->error. */ rc = wolfSSH_get_error(ssh); @@ -1045,8 +1268,11 @@ static int ssh_worker(thread_ctx_t* threadCtx) * 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) { + * still producing, so its EOF waits for the child to exit. + * A claimed session only: unclaimed, shellCtx.channelId is + * still 0, which is the first channel the peer is given. */ + if (!eofAnswered && echoOnly + && threadCtx->shellCtx.state == APP_STATE_CONNECTED) { WOLFSSH_CHANNEL* eofChannel; eofChannel = wolfSSH_ChannelFind(ssh, @@ -1113,7 +1339,8 @@ static int ssh_worker(thread_ctx_t* threadCtx) * wolfSSH_ChannelIdRead() has no isKeying gate; the window * credit it owes is parked until the rekey finishes. */ if (rc == WS_CHAN_RXD || rc == WS_REKEYING) { - if (lastChannel == threadCtx->shellCtx.channelId) { + if (threadCtx->shellCtx.state == APP_STATE_CONNECTED && + lastChannel == threadCtx->shellCtx.channelId) { cnt_r = wolfSSH_ChannelIdRead(ssh, threadCtx->shellCtx.channelId, threadCtx->channelBuffer, @@ -1130,7 +1357,8 @@ static int ssh_worker(thread_ctx_t* threadCtx) #endif #ifdef WOLFSSH_SHELL if (!threadCtx->echo) { - cnt_w = (int)write(childFd, + cnt_w = (int)write( + threadCtx->shellCtx.appFd, threadCtx->channelBuffer, cnt_r); } else { @@ -1243,6 +1471,14 @@ static int ssh_worker(thread_ctx_t* threadCtx) * above, which has already run this pass. */ continue; } + else if (rc == WS_WANT_WRITE) { + /* The send is owed, not lost: wait for the socket to + * take it. Application-driven mode answers session + * requests here, so a blocked reply would otherwise + * end a session accept() used to carry through. */ + wantWrite = 1; + continue; + } else if (rc != WS_WANT_READ) { #ifdef SHELL_DEBUG printf("Break:read sshFd returns %d: errno =%x\n", @@ -1252,11 +1488,11 @@ static int ssh_worker(thread_ctx_t* threadCtx) } } } - #ifdef WOLFSSH_SHELL - if (!threadCtx->echo) { - if (FD_ISSET(childFd, &readFds)) { - cnt_r = (int)read(childFd, + if (threadCtx->shellCtx.state == APP_STATE_CONNECTED + && threadCtx->shellCtx.appFd >= 0) { + if (FD_ISSET(threadCtx->shellCtx.appFd, &readFds)) { + cnt_r = (int)read(threadCtx->shellCtx.appFd, threadCtx->shellCtx.buffer, sizeof threadCtx->shellCtx.buffer); /* This read will return 0 on EOF */ @@ -1335,12 +1571,13 @@ static int ssh_worker(thread_ctx_t* threadCtx) } } } - if (threadCtx->agentCtx.state == APP_STATE_LISTEN) { - if (FD_ISSET(agentListenFd, &readFds)) { + if (threadCtx->agentCtx.state == APP_STATE_LISTEN + && threadCtx->agentCtx.listenFd >= 0) { + if (FD_ISSET(threadCtx->agentCtx.listenFd, &readFds)) { #ifdef SHELL_DEBUG printf("accepting agent connection\n"); #endif - agentFd = accept(agentListenFd, NULL, NULL); + agentFd = accept(threadCtx->agentCtx.listenFd, NULL, NULL); if (agentFd == -1) { rc = errno; if (rc != SOCKET_EWOULDBLOCK) { @@ -1371,8 +1608,7 @@ static int ssh_worker(thread_ctx_t* threadCtx) fwdFd = -1; threadCtx->fwdCtx.appFd = -1; if (threadCtx->fwdCbCtx.hostName != NULL) { - WFREE(threadCtx->fwdCbCtx.hostName, - NULL, 0); + WFREE(threadCtx->fwdCbCtx.hostName, NULL, 0); threadCtx->fwdCbCtx.hostName = NULL; } threadCtx->fwdCtx.state = APP_STATE_LISTEN; @@ -1497,8 +1733,10 @@ static int ssh_worker(thread_ctx_t* threadCtx) #endif /* WOLFSSH_FWD */ } #ifdef WOLFSSH_SHELL - if (!threadCtx->echo) - WCLOSESOCKET(childFd); + if (threadCtx->shellCtx.appFd >= 0) { + WCLOSESOCKET(threadCtx->shellCtx.appFd); + threadCtx->shellCtx.appFd = -1; + } #endif } @@ -1506,10 +1744,13 @@ static int ssh_worker(thread_ctx_t* threadCtx) pthread_join(globalReq_th, NULL); #endif - return 0; + return workerRet; } +/* Seconds to wait on the socket between sftp and scp accept attempts. */ +#define ES_ACCEPT_TIMEOUT 1 + #ifdef WOLFSSH_SFTP #define TEST_SFTP_TIMEOUT_SHORT 0 @@ -1756,8 +1997,10 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs) else { ret = NonBlockSSH_accept(threadCtx->ssh); } + #ifdef WOLFSSH_SCP - /* finish off SCP operation */ + /* The legacy path: accept() reports the scp command and does the + * transfer on re-entry. */ if (ret == WS_SCP_INIT) { if (!threadCtx->nonBlock) ret = wolfSSH_accept(threadCtx->ssh); @@ -1773,6 +2016,8 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs) break; #ifdef WOLFSSH_SFTP + /* The legacy path: wolfSSH_accept() ran the subsystem request + * itself and handed back a session ready to serve. */ case WS_SFTP_COMPLETE: ret = sftp_worker(threadCtx); break; @@ -1780,6 +2025,48 @@ static THREAD_RETURN WOLFSSH_THREAD server_worker(void* vArgs) case WS_SUCCESS: ret = ssh_worker(threadCtx); + #ifdef WOLFSSH_SCP + if (ret == WS_SCP_INIT) { + /* On a non-blocking socket the transfer comes back part + * done; resume it rather than tearing the session down + * mid-file. */ + do { + ret = wolfSSH_SCP_accept(threadCtx->ssh); + error = wolfSSH_get_error(threadCtx->ssh); + if (ret != WS_SCP_COMPLETE + && (error == WS_WANT_READ + || error == WS_WANT_WRITE)) { + tcp_select(wolfSSH_get_fd(threadCtx->ssh), + ES_ACCEPT_TIMEOUT); + } + } while (ret != WS_SCP_COMPLETE + && (error == WS_WANT_READ || error == WS_WANT_WRITE)); + if (ret == WS_SCP_COMPLETE) { + printf("scp file transfer completed\n"); + ret = 0; + } + } + #endif + #ifdef WOLFSSH_SFTP + if (ret == WS_SFTP_COMPLETE) { + do { + ret = wolfSSH_SFTP_accept(threadCtx->ssh); + error = wolfSSH_get_error(threadCtx->ssh); + /* Wait on the socket between attempts; without this the + * gap before the client's SFTP INIT is a busy spin. */ + if (ret != WS_SFTP_COMPLETE + && (error == WS_WANT_READ + || error == WS_WANT_WRITE)) { + tcp_select(wolfSSH_get_fd(threadCtx->ssh), + ES_ACCEPT_TIMEOUT); + } + } while (ret != WS_SFTP_COMPLETE + && (error == WS_WANT_READ || error == WS_WANT_WRITE)); + } + if (ret == WS_SFTP_COMPLETE) { + ret = sftp_worker(threadCtx); + } + #endif break; } @@ -3139,6 +3426,7 @@ static void ShowUsage(void) #ifdef WOLFSSH_SHELL printf(" -f echo input\n"); #endif + printf(" -A drive channels from the application callbacks\n"); printf(" -p port to connect on, default %d\n", wolfSshPort); printf(" -N use non-blocking sockets\n"); #ifdef WOLFSSH_SFTP @@ -3192,7 +3480,7 @@ static void ShowUsage(void) } -#define ECHOSERVER_OPTLIST "?1a:d:DefEp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:G:HW:" +#define ECHOSERVER_OPTLIST "?1a:Ad:DefEp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:G:HW:" #ifdef WOLFSSH_WINDOWS_CERT_STORE /* Detects whether argv or the environment requests a host key from the @@ -3323,6 +3611,7 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) int userEcc = 0; int peerEcc = 0; int echo = 0; + int appChannels = 0; int ch; word16 port = wolfSshPort; char* readyFile = NULL; @@ -3384,6 +3673,10 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) #endif break; + case 'A': + appChannels = 1; + break; + case 'p': if (myoptarg == NULL) { ES_ERROR("NULL port value\n"); @@ -3625,6 +3918,25 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) #ifdef WOLFSSH_FWD wolfSSH_CTX_SetFwdCb(ctx, wolfSSH_FwdDefaultActions, NULL); #endif + /* With -A the echoserver drives its own channels: accept() stops at + * userauth and these callbacks start the shell, subsystem or transfer. + * Off by default, so the path this example has always taken keeps an + * in-tree demo. The two are exclusive: the callbacks answer the session + * requests the accept state machine would otherwise answer itself. */ + /* The shell callback is the only place the pty is forked, and an exec + * request that is not a transfer runs as a session, so both are + * registered in both modes. accept() honours a registered callback with + * application-driven channels off, so the legacy path keeps the shell it + * has always started for either request. The subsystem callback is not + * registered there: accept() serves sftp itself. */ + wolfSSH_CTX_SetChannelReqShellCb(ctx, wsShellStartCb); + wolfSSH_CTX_SetChannelReqExecCb(ctx, wsExecStartCb); + if (appChannels) { + wolfSSH_CTX_SetAppChannels(ctx, 1); +#ifdef WOLFSSH_SFTP + wolfSSH_CTX_SetChannelReqSubsysCb(ctx, wsSubsysStartCb); +#endif + } #ifndef NO_FILESYSTEM if (sshPubKeyList) { @@ -4044,6 +4356,7 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) #endif wolfSSH_SetUserAuthCtx(ssh, &pwMapList); wolfSSH_SetKeyingCompletionCbCtx(ssh, (void*)ssh); + wolfSSH_SetChannelReqCtx(ssh, (void*)threadCtx); /* Use the session object for its own highwater callback ctx */ if (defaultHighwater > 0) { @@ -4103,13 +4416,13 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) tcp_set_nonblocking(&clientFd); wolfSSH_set_fd(ssh, (int)clientFd); + threadCtx->fd = clientFd; #if defined(WOLFSSL_PTHREADS) && defined(WOLFSSL_TEST_GLOBAL_REQ) threadCtx->ctx = ctx; #endif threadCtx->ssh = ssh; - threadCtx->fd = clientFd; - threadCtx->id = threadCount++; + threadCtx->tid = threadCount++; threadCtx->nonBlock = nonBlock; threadCtx->echo = echo; threadCtx->shellCtx.privateData = NULL; diff --git a/scripts/scp.test b/scripts/scp.test index 20f9737eb..cfa69739c 100755 --- a/scripts/scp.test +++ b/scripts/scp.test @@ -137,6 +137,67 @@ else exit 1 fi +# With -A the echoserver binds the scp command itself and runs the transfer +# through wolfSSH_SCP_accept(); the cases above take the accept() re-entry +# instead. -N reaches that call's want-read/want-write retry path, which a +# blocking server completes in one call. The client stays blocking, which the +# server does not care about. +echo "Test basic copy from server to local, app-driven server" +./examples/echoserver/echoserver -A -N -1 -R $ready_file & +server_pid=$! +create_port +$run_client ./examples/scpclient/wolfscp -u jill -P upthehill -p $port -S $PWD/scripts/scp.test:$PWD/scp.test +RESULT=$? +remove_ready_file +stop_server +check_timeout $RESULT "basic copy from server to local, app-driven server" + +if test -e $PWD/scp.test; then + rm $PWD/scp.test +else + echo -e "\n\nfailed to get file from app-driven server" + do_cleanup + exit 1 +fi + +echo "Test basic copy from local to server, app-driven server" +./examples/echoserver/echoserver -A -N -1 -R $ready_file & +server_pid=$! +create_port +$run_client ./examples/scpclient/wolfscp -u jill -P upthehill -p $port -L $PWD/scripts/scp.test:$PWD/scp.test +RESULT=$? +remove_ready_file +stop_server +check_timeout $RESULT "basic copy from local to server, app-driven server" + +if test -e $PWD/scp.test; then + rm $PWD/scp.test +else + echo -e "\n\nfailed to send file to app-driven server" + do_cleanup + exit 1 +fi + +# The same handoff on a blocking server, where wolfSSH_SCP_accept() runs to +# completion in one call rather than through the retry loop above. +echo "Test basic copy from server to local, blocking app-driven server" +./examples/echoserver/echoserver -A -1 -R $ready_file & +server_pid=$! +create_port +$run_client ./examples/scpclient/wolfscp -u jill -P upthehill -p $port -S $PWD/scripts/scp.test:$PWD/scp.test +RESULT=$? +remove_ready_file +stop_server +check_timeout $RESULT "basic copy from server to local, blocking app-driven server" + +if test -e $PWD/scp.test; then + rm $PWD/scp.test +else + echo -e "\n\nfailed to get file from blocking app-driven server" + do_cleanup + exit 1 +fi + echo "Test of getting empty file" touch $PWD/scripts/empty ./examples/echoserver/echoserver -1 -R $ready_file & diff --git a/scripts/sftp.test b/scripts/sftp.test index 3d5304cfe..9e551df15 100755 --- a/scripts/sftp.test +++ b/scripts/sftp.test @@ -112,6 +112,41 @@ if [ $RESULT -ne 0 ]; then exit 1 fi +# With -A the echoserver answers the subsystem request in its own callback +# and serves the session through wolfSSH_SFTP_accept(); the cases above take +# the accept() re-entry instead. +if [ "$nonblockingOnly" = 0 ]; then + echo "Test connection to an app-driven server" + ./examples/echoserver/echoserver -A -1 -R "$ready_file" & + server_pid=$! + create_port + echo "exit" | ./examples/sftpclient/wolfsftp -u jill -P upthehill -p "$port" + RESULT=$? + remove_ready_file + if [ $RESULT -ne 0 ]; then + echo + echo "failed to connect to app-driven server" + do_cleanup + exit 1 + fi +fi + +# The same handoff on a non-blocking server, which reaches the accept call's +# want-read and want-write retries. +echo "Test non blocking connection to an app-driven server" +./examples/echoserver/echoserver -A -N -1 -R "$ready_file" & +server_pid=$! +create_port +echo "exit" | ./examples/sftpclient/wolfsftp -N -u jill -P upthehill -p "$port" +RESULT=$? +remove_ready_file +if [ $RESULT -ne 0 ]; then + echo + echo "failed to connect to non blocking app-driven server" + do_cleanup + exit 1 +fi + # Test want write return from highwater callback if [ "$nonblockingOnly" = 0 ]; then echo "Test want write return from highwater callback" diff --git a/scripts/sshclient.test b/scripts/sshclient.test index af145f968..762f457fe 100755 --- a/scripts/sshclient.test +++ b/scripts/sshclient.test @@ -83,6 +83,7 @@ trap do_trap INT TERM # -f keeps the server in echo mode. Without it a build with shell support # tries to fork a login shell for the user, which fails since jill isn't a # real account, and the session ends before anything crosses the channel. +# Any arguments are passed on to the echoserver. start_server() { # The -1 server exits after its connection, but a client run that failed # before connecting leaves one listening. Reap it, server_pid is about @@ -95,7 +96,7 @@ start_server() { fi rm -f "$ready_file" - ./examples/echoserver/echoserver -1 -f -R "$ready_file" \ + ./examples/echoserver/echoserver -1 -f "$@" -R "$ready_file" \ > "$work_dir/server.log" 2>&1 & server_pid=$! @@ -282,6 +283,25 @@ if [ -s "$work_dir/terminal.log" ]; then && fail "the client sent a command it wasn't given" fi +# -A hands the session requests to the echoserver's own callbacks: the shell +# callback claims the channel the worker echoes on, and an exec request that +# is not a transfer runs as a session through the same callback. +echo "Test terminal session, app-driven server" +start_server -A +run_client "" -E "$work_dir/appterm.log" -p $port jill@127.0.0.1 +[ $? -ne 0 ] && fail "failed to open the terminal session on an app-driven server" + +grep -q "hello" "$client_out" \ + || fail "the app-driven echoserver's reply didn't make it back" + +echo "Test a session given a command, app-driven server" +start_server -A +run_client "" -E "$work_dir/appcommand.log" -p $port jill@127.0.0.1 "echo three" +[ $? -ne 0 ] && fail "failed to open the session on an app-driven server" + +grep -q "hello" "$client_out" \ + || fail "the app-driven echoserver's reply didn't make it back" + echo "Test the usage message" ./apps/wolfssh/wolfssh -Z 2>&1 | grep -q "usage:" \ || fail "no usage message for a bad option" diff --git a/tests/api.c b/tests/api.c index 55009434c..1d309a8f3 100644 --- a/tests/api.c +++ b/tests/api.c @@ -2611,6 +2611,8 @@ static void test_wolfSSH_SCP_CB(void) AssertIntEQ(wolfSSH_SetScpErrorMsg(NULL, err), WS_BAD_ARGUMENT); AssertIntEQ(wolfSSH_SetScpErrorMsg(ssh, NULL), WS_BAD_ARGUMENT); + AssertIntEQ(wolfSSH_SCP_accept(NULL), WS_BAD_ARGUMENT); + wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); } @@ -8004,6 +8006,9 @@ static void test_wolfSSH_KeyboardInteractive(void) argsCount = 0; args[argsCount++] = "."; args[argsCount++] = "-1"; + /* Echo mode: "test" is not an account on the host, so the echoserver's + * shell callback would refuse the shell request this client sends. */ + args[argsCount++] = "-f"; args[argsCount++] = "-i"; args[argsCount++] = "test:test"; args[argsCount++] = "-p";