diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 2212b1e7e..cc63b7a07 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,17 @@ typedef struct { WS_FwdCbActionCtx fwdCbCtx; #endif WS_AppCtx shellCtx; +#ifdef WOLFSSH_SHELL + /* The forked shell. Held here because the callback that forks is gone + * by the time the session ends. */ + pid_t shellPid; +#endif +#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 +271,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 +680,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 +690,302 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx, #endif /* WOLFSSH_FWD */ +#ifdef WOLFSSH_SHELL +static void ChildSig(int sig) +{ + (void)sig; + ChildRunning = 0; +} + + +/* End a forked shell and its pty. A shell left behind runs on behind a pty + * nothing reads, and its exit clears ChildRunning out from under a worker + * loop -- this connection's, or another's, since the flag is shared. */ +static void ShellChildCleanup(thread_ctx_t* threadCtx) +{ + if (threadCtx->shellCtx.appFd >= 0) { + WCLOSESOCKET(threadCtx->shellCtx.appFd); + threadCtx->shellCtx.appFd = -1; + } + + if (threadCtx->shellPid > 0) { + void (*prevSig)(int); + + /* This exit is ours, not the session's. */ + prevSig = signal(SIGCHLD, SIG_DFL); + kill(threadCtx->shellPid, SIGKILL); + waitpid(threadCtx->shellPid, NULL, 0); + signal(SIGCHLD, prevSig); + threadCtx->shellPid = -1; + } +} + + +#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; +} + + +#if defined(WOLFSSH_SFTP) || defined(WOLFSSH_SCP) +/* wolfSSH_SFTP_accept() and wolfSSH_SCP_accept() work from the head of the + * channel list, so a transfer granted on any other channel answers the peer + * success and then runs against the wrong one. The shell has no such limit: + * its callback keeps the channel id. */ +static int TransferChannel(const thread_ctx_t* threadCtx, + WOLFSSH_CHANNEL* channel) +{ + return channel == wolfSSH_ChannelNext(threadCtx->ssh, NULL); +} +#endif /* WOLFSSH_SFTP || WOLFSSH_SCP */ + + +/* 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 + /* The child is the connection's from here, so every exit below can + * end it. */ + threadCtx->shellPid = childPid; + + rc = tcgetattr(threadCtx->shellCtx.appFd, &tios); + if (rc != 0) { + printf("tcgetattr failed: rc =%d,errno=%x\n", rc, errno); + ShellChildCleanup(threadCtx); + return 1; + } + rc = tcsetattr(threadCtx->shellCtx.appFd, TCSAFLUSH, &tios); + if (rc != 0) { + printf("tcsetattr failed: rc =%d,errno=%x\n", rc, errno); + ShellChildCleanup(threadCtx); + 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 + && TransferChannel(threadCtx, channel)) { + 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) { + /* An scp command the transfer cannot be run for is refused + * rather than served as a session. */ + if (TransferChannel((thread_ctx_t*)vCtx, channel)) { + ((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 +1027,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 +1111,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 +1126,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 +1146,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 +1176,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 +1188,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 +1202,44 @@ static int ssh_worker(thread_ctx_t* threadCtx) FD_SET(sshFd, &readFds); maxFd = sshFd; + #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. It + * runs ahead of the write set: an open the socket would not + * take has to reach that set this pass, or the wait below is + * for readability alone and the peer is waiting on the open. */ + if (!agentOpened) { + int agentRc = wolfSSH_AGENT_ChannelOpen(ssh); + + if (agentRc == WS_SUCCESS) + agentOpened = 1; + else if (agentRc == WS_WANT_WRITE) + wantWrite = 1; + } + #endif + + FD_ZERO(&writeFds); + if (wantWrite) + FD_SET(sshFd, &writeFds); + #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 +1264,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 +1283,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 /* The channel reads below overwrite cnt_r with a byte * count, so keep the worker's status. */ rc = cnt_r; @@ -1042,8 +1302,11 @@ static int ssh_worker(thread_ctx_t* threadCtx) /* The peer is done sending: hand back the backlog and answer * its EOF, since the library no longer answers for us. Off * the channel's own state, not the once-only WS_EOF status. - * Echo mode only; a shell child on a pty still produces. */ - if (!eofAnswered && echoOnly) { + * Echo mode only; a shell child on a pty still produces. + * 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, @@ -1110,7 +1373,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, @@ -1127,7 +1391,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 { @@ -1241,7 +1506,12 @@ static int ssh_worker(thread_ctx_t* threadCtx) continue; } else if (rc == WS_WANT_WRITE) { - /* Transient; the queue drives the write side. */ + /* 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_FATAL_ERROR || (wolfSSH_get_error(ssh) != WS_WANT_READ @@ -1254,11 +1524,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 */ @@ -1337,12 +1607,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) { @@ -1373,8 +1644,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; @@ -1499,8 +1769,7 @@ static int ssh_worker(thread_ctx_t* threadCtx) #endif /* WOLFSSH_FWD */ } #ifdef WOLFSSH_SHELL - if (!threadCtx->echo) - WCLOSESOCKET(childFd); + ShellChildCleanup(threadCtx); #endif } @@ -1508,10 +1777,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 @@ -1762,8 +2034,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); @@ -1779,6 +2053,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; @@ -1786,9 +2062,57 @@ 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; } +#ifdef WOLFSSH_SHELL + /* The session request is answered inside wolfSSH_accept(), so a reply + * that fails leaves a forked shell with no ssh_worker() to end it. */ + ShellChildCleanup(threadCtx); +#endif + if (ret == WS_FATAL_ERROR) { const char* errorStr; error = wolfSSH_get_error(threadCtx->ssh); @@ -3145,6 +3469,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 @@ -3198,7 +3523,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 @@ -3329,6 +3654,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; @@ -3390,6 +3716,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"); @@ -3631,6 +3961,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) { @@ -4050,6 +4399,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) { @@ -4109,19 +4459,22 @@ 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; threadCtx->shellCtx.listenFd = -1; threadCtx->shellCtx.appFd = -1; threadCtx->shellCtx.state = APP_STATE_INIT; +#ifdef WOLFSSH_SHELL + threadCtx->shellPid = -1; +#endif #ifdef WOLFSSH_AGENT threadCtx->agentCtx.privateData = &threadCtx->agentCbCtx; threadCtx->agentCtx.listenFd = -1; 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..90fcdd199 100755 --- a/scripts/sftp.test +++ b/scripts/sftp.test @@ -6,6 +6,7 @@ no_pid=-1 server_pid="$no_pid" ready_file="$PWD/wolfssh_sftp_ready$$" nonblockingOnly=0 +blockBuild=0 [ ! -x ./examples/sftpclient/wolfsftp ] && echo && echo "wolfSFTP client doesn't exist" && exit 1 @@ -27,6 +28,7 @@ if echo "$WOLFSSH_OPTIONS" | grep -qx "TEST_BLOCK" then echo "macro WOLFSSH_TEST_BLOCK was used" nonblockingOnly=1 + blockBuild=1 fi #echo "ready file $ready_file" @@ -112,6 +114,46 @@ 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. Held out of a forced-blocking build: the +# two ends deadlock there, the server parked in DoReceive with output it owes +# still queued, which is a defect of its own rather than this case failing. +if [ "$blockBuild" = 0 ]; then + 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 +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 5635e2a5a..fa14cf980 100644 --- a/tests/api.c +++ b/tests/api.c @@ -2615,6 +2615,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); } @@ -8008,6 +8010,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";