diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 00320b7c8..2e7145df8 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -71,6 +71,10 @@ #include #endif +/* ChildRunning is volatile sig_atomic_t whether or not a shell is compiled + * in, and ssh_worker() reads it unguarded. */ +#include + #ifdef WOLFSSH_SHELL #ifdef HAVE_PTY_H #include @@ -84,7 +88,6 @@ #ifndef USE_WINDOWS_API #include #endif - #include #if defined(__QNX__) || defined(__QNXNTO__) #include #include @@ -126,6 +129,7 @@ static int quit = 0; wolfSSL_Mutex doneLock; #define MAX_PASSWD_RETRY 3 static int passwdRetry = MAX_PASSWD_RETRY; +static volatile sig_atomic_t ChildRunning = 0; #ifndef EXAMPLE_HIGHWATER_MARK @@ -184,7 +188,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) @@ -199,6 +203,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. */ @@ -237,7 +247,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); @@ -635,8 +645,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; } @@ -644,6 +655,214 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx, #endif /* WOLFSSH_FWD */ +#ifdef WOLFSSH_SHELL +static void ChildSig(int sig) +{ + (void)sig; + ChildRunning = 0; +} + + +#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 */ + + +/* 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; + } + + /* 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) { + /* forkpty failed, so return */ + ChildRunning = 0; + 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 + signal(SIGCHLD, ChildSig); + + rc = tcgetattr(threadCtx->shellCtx.appFd, &tios); + if (rc != 0) { + printf("tcgetattr failed: rc =%d,errno=%x\n", rc, errno); + return 1; + } + rc = tcsetattr(threadCtx->shellCtx.appFd, TCSAFLUSH, &tios); + if (rc != 0) { + printf("tcsetattr failed: rc =%d,errno=%x\n", rc, errno); + return 1; + } + + #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; + 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. */ + if (type == WOLFSSH_SESSION_SUBSYSTEM && cmd != NULL + && 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); + +#ifdef WOLFSSH_SCP + if (cmd != NULL && WSTRNCMP(cmd, "scp ", 4) == 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, @@ -685,30 +904,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 */ @@ -793,16 +988,6 @@ 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; @@ -815,11 +1000,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; @@ -838,6 +1020,20 @@ static int ssh_worker(thread_ctx_t* threadCtx) sshFd = wolfSSH_get_fd(ssh); + if (threadCtx->shellCtx.state != APP_STATE_CONNECTED) { + /* The legacy path: wolfSSH_accept() answered the session request + * itself, so no channel-request callback ran to claim the channel. + * Claim it here, on the session accept() established. */ + WOLFSSH_CHANNEL* sessionChannel; + + sessionChannel = wolfSSH_ChannelNext(ssh, NULL); + if (sessionChannel != NULL) { + 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); @@ -845,54 +1041,8 @@ 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 #ifdef WOLFSSH_AGENT WS_SOCKET_T agentFd = -1; WS_SOCKET_T agentListenFd = threadCtx->agentCtx.listenFd; @@ -903,52 +1053,7 @@ 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; @@ -960,11 +1065,23 @@ 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. */ + 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 @@ -996,6 +1113,7 @@ static int ssh_worker(thread_ctx_t* threadCtx) maxFd = fwdFd; } #endif /* WOLFSSH_FWD */ + rc = select((int)maxFd + 1, &readFds, NULL, NULL, NULL); if (rc == -1) { break; @@ -1011,6 +1129,16 @@ 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) { + return WS_SFTP_COMPLETE; + } + #endif + #ifdef WOLFSSH_SCP + if (threadCtx->doScp) { + return WS_SCP_INIT; + } + #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); @@ -1089,7 +1217,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, @@ -1106,7 +1235,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 { @@ -1205,7 +1335,7 @@ static int ssh_worker(thread_ctx_t* threadCtx) * above, which has already run this pass. */ continue; } - else if (rc != WS_WANT_READ) { + else if (rc != WS_WANT_READ && rc != WS_REKEYING) { #ifdef SHELL_DEBUG printf("Break:read sshFd returns %d: errno =%x\n", cnt_r, errno); @@ -1214,11 +1344,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 */ @@ -1333,8 +1463,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; @@ -1459,8 +1588,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 } @@ -1472,6 +1603,9 @@ static int ssh_worker(thread_ctx_t* threadCtx) } +/* Seconds to wait on the socket between subsystem-accept attempts. */ +#define ES_ACCEPT_TIMEOUT 1 + #ifdef WOLFSSH_SFTP #define TEST_SFTP_TIMEOUT_SHORT 0 @@ -1718,8 +1852,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); @@ -1735,6 +1871,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; @@ -1742,6 +1880,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; } @@ -3101,6 +3281,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 @@ -3238,6 +3419,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; @@ -3260,7 +3442,8 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) #endif if (argc > 0) { - const char* optlist = "?1a:d:DefEp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:G:H"; + const char* optlist = + "?1a:Ad:DefEp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:G:H"; myoptind = 0; while ((ch = mygetopt(argc, argv, optlist)) != -1) { switch (ch) { @@ -3297,6 +3480,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"); @@ -3502,6 +3689,22 @@ 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, so it is + * registered in both modes. accept() honours a registered callback with + * application-driven channels off, so the legacy path keeps its shell. */ + wolfSSH_CTX_SetChannelReqShellCb(ctx, wsShellStartCb); + if (appChannels) { + wolfSSH_CTX_SetAppChannels(ctx, 1); +#ifdef WOLFSSH_SFTP + wolfSSH_CTX_SetChannelReqSubsysCb(ctx, wsSubsysStartCb); +#endif + wolfSSH_CTX_SetChannelReqExecCb(ctx, wsExecStartCb); + } #ifndef NO_FILESYSTEM if (sshPubKeyList) { @@ -3876,6 +4079,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) { @@ -3935,13 +4139,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/src/agent.c b/src/agent.c index 33c0eb936..abfbf63e3 100644 --- a/src/agent.c +++ b/src/agent.c @@ -1731,6 +1731,71 @@ int wolfSSH_AGENT_enable(WOLFSSH* ssh, byte isEnabled) } +int wolfSSH_AGENT_ChannelOpen(WOLFSSH* ssh) +{ + WOLFSSH_AGENT_CTX* newAgent = NULL; + WOLFSSH_CHANNEL* newChannel = NULL; + int ret = WS_SUCCESS; + + WLOG_ENTER(); + + if (ssh == NULL) + ret = WS_SSH_NULL_E; + else if (!ssh->useAgent) { + /* Nothing asked for agent forwarding on this session. */ + ret = WS_BAD_ARGUMENT; + } + else if (ssh->agent == NULL) { + /* Server side sets ssh->agent here and nowhere else, so a NULL one + * is the "not opened yet" test. Idempotent so a caller polling for + * the peer's request cannot end up with two agent channels. */ + WLOG(WS_LOG_AGENT, "Starting agent channel"); + + newAgent = wolfSSH_AGENT_new(ssh->ctx->heap); + if (newAgent == NULL) + ret = WS_MEMORY_E; + + if (ret == WS_SUCCESS) { + newChannel = ChannelNew(ssh, ID_CHANTYPE_AUTH_AGENT, + ssh->ctx->windowSz, ssh->ctx->maxPacketSz); + if (newChannel == NULL) + ret = WS_MEMORY_E; + } + + if (ret == WS_SUCCESS) { + ret = SendChannelOpenSession(ssh, newChannel); + + if (ret < WS_SUCCESS + && ret != WS_WANT_WRITE && ret != WS_WANT_READ) { + ChannelDelete(newChannel, ssh->ctx->heap); + } + else { + /* Publish the agent even when the open is only queued, so + * a retry takes the already-open path above rather than + * opening a second channel. */ + ChannelAppend(ssh, newChannel); + newAgent->channel = newChannel->channel; + ssh->agent = newAgent; + newAgent = NULL; + if (ssh->ctx->agentCb) { + ssh->ctx->agentCb(WOLFSSH_AGENT_LOCAL_SETUP, + ssh->agentCbCtx); + } + } + } + + if (newAgent != NULL) + wolfSSH_AGENT_free(newAgent); + } + + if (ssh != NULL) + ssh->error = ret; + + WLOG_LEAVE(ret); + return ret; +} + + int wolfSSH_AGENT_worker(WOLFSSH* ssh) { int ret = WS_SUCCESS; diff --git a/src/internal.c b/src/internal.c index 10769aef4..e42cf3814 100644 --- a/src/internal.c +++ b/src/internal.c @@ -1666,6 +1666,7 @@ WOLFSSH* SshInit(WOLFSSH* ssh, WOLFSSH_CTX* ctx) ssh->highwaterMark = ctx->highwaterMark; ssh->msgHighwaterMark = ctx->msgHighwaterMark; ssh->maxAuthAttempts = ctx->maxAuthAttempts; + ssh->appChannels = ctx->appChannels; ssh->highwaterCtx = (void*)ssh; ssh->reqSuccessCtx = (void*)ssh; ssh->fs = NULL; @@ -12675,6 +12676,9 @@ static int DoChannelRequest(WOLFSSH* ssh, if (ssh->ctx->channelReqShellCb) { rej = ssh->ctx->channelReqShellCb(channel, ssh->channelReqCtx); } + else { + rej = ssh->appChannels; + } ssh->clientState = CLIENT_DONE; } else if (ChannelRequestIs(type, typeSz, "exec")) { @@ -12684,6 +12688,9 @@ static int DoChannelRequest(WOLFSSH* ssh, if (ssh->ctx->channelReqExecCb) { rej = ssh->ctx->channelReqExecCb(channel, ssh->channelReqCtx); } + else { + rej = ssh->appChannels; + } ssh->clientState = CLIENT_DONE; WLOG(WS_LOG_DEBUG, " command = %s", channel->command); @@ -12695,6 +12702,9 @@ static int DoChannelRequest(WOLFSSH* ssh, if (ssh->ctx->channelReqSubsysCb) { rej = ssh->ctx->channelReqSubsysCb(channel, ssh->channelReqCtx); } + else { + rej = ssh->appChannels; + } ssh->clientState = CLIENT_DONE; WLOG(WS_LOG_DEBUG, " subsystem = %s", channel->command); @@ -12836,7 +12846,7 @@ static int DoChannelRequest(WOLFSSH* ssh, int replyRet; if (rej) { - WLOG(WS_LOG_DEBUG, "Callback rejecting channel request."); + WLOG(WS_LOG_DEBUG, "Rejecting channel request."); } replyRet = SendChannelSuccess(ssh, channelId, (ret == WS_SUCCESS && !rej)); diff --git a/src/ssh.c b/src/ssh.c index ae7d0b2a0..4a26809fc 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -579,6 +579,8 @@ const char acceptState[] = "accept state: %s"; int wolfSSH_accept(WOLFSSH* ssh) { + byte stopState; + WLOG(WS_LOG_DEBUG, "Entering wolfSSH_accept()"); if (ssh == NULL) @@ -598,6 +600,15 @@ int wolfSSH_accept(WOLFSSH* ssh) return WS_INVALID_STATE_E; } + /* In application-driven mode the state machine stops as soon as the + * user is authenticated; everything past that is the application's. + * Only stop there if the session has not already gone by: the loop + * below tests the stop state exactly, so a state it has stepped over + * would never terminate it. */ + stopState = (ssh->appChannels + && ssh->acceptState <= ACCEPT_SERVER_USERAUTH_SENT) ? + ACCEPT_SERVER_USERAUTH_SENT : ACCEPT_CLIENT_SESSION_ESTABLISHED; + /* check if data pending to be sent */ if (ssh->outputBuffer.length > 0 && ssh->acceptState < ACCEPT_CLIENT_SESSION_ESTABLISHED) { @@ -609,7 +620,11 @@ int wolfSSH_accept(WOLFSSH* ssh) ssh->acceptState != ACCEPT_SERVER_USERAUTH_ACCEPT_SENT && ssh->acceptState != ACCEPT_SERVER_KEXINIT_SENT && ssh->acceptState != ACCEPT_KEYED && - ssh->acceptState != ACCEPT_SERVER_CHANNEL_ACCEPT_SENT) { + ssh->acceptState != ACCEPT_SERVER_CHANNEL_ACCEPT_SENT && + /* Never step over where this call is meant to stop. The + * loop below tests for that state exactly, and the SCP and + * SFTP re-entry states sort after it. */ + ssh->acceptState != stopState) { WLOG(WS_LOG_DEBUG, "Advancing accept state"); ssh->acceptState++; } @@ -631,7 +646,7 @@ int wolfSSH_accept(WOLFSSH* ssh) } } - while (ssh->acceptState != ACCEPT_CLIENT_SESSION_ESTABLISHED) { + while (ssh->acceptState != stopState) { switch (ssh->acceptState) { case ACCEPT_BEGIN: @@ -721,6 +736,12 @@ int wolfSSH_accept(WOLFSSH* ssh) } ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; WLOG(WS_LOG_DEBUG, acceptState, "SERVER_USERAUTH_SENT"); + if (stopState == ACCEPT_SERVER_USERAUTH_SENT) { + /* The application takes it from here. Tested through + * stopState so a callback that changed the flag during + * this call cannot half-apply it. */ + break; + } FALL_THROUGH; case ACCEPT_SERVER_USERAUTH_SENT: @@ -764,52 +785,12 @@ int wolfSSH_accept(WOLFSSH* ssh) #endif /* WOLFSSH_SFTP and !NO_WOLFSSH_SERVER */ #ifdef WOLFSSH_AGENT if (ssh->useAgent) { - WOLFSSH_AGENT_CTX* newAgent; - WOLFSSH_CHANNEL* newChannel; - - WLOG(WS_LOG_AGENT, "Starting agent channel"); - - newAgent = wolfSSH_AGENT_new(ssh->ctx->heap); - if (newAgent == NULL) { - ssh->error = WS_MEMORY_E; - WLOG(WS_LOG_DEBUG, acceptError, - "SERVER_USERAUTH_ACCEPT_DONE", ssh->error); - return WS_ERROR; - } - - newChannel = ChannelNew(ssh, ID_CHANTYPE_AUTH_AGENT, - ssh->ctx->windowSz, ssh->ctx->maxPacketSz); - if (newChannel == NULL) { - wolfSSH_AGENT_free(newAgent); - ssh->error = WS_MEMORY_E; - WLOG(WS_LOG_DEBUG, acceptError, - "SERVER_USERAUTH_ACCEPT_DONE", ssh->error); - return WS_FATAL_ERROR; - } - - ssh->error = SendChannelOpenSession(ssh, newChannel); + ssh->error = wolfSSH_AGENT_ChannelOpen(ssh); if (ssh->error < WS_SUCCESS) { - if (ssh->error == WS_WANT_WRITE || - ssh->error == WS_WANT_READ) { - ChannelAppend(ssh, newChannel); - } - else { - ChannelDelete(newChannel, ssh->ctx->heap); - wolfSSH_AGENT_free(newAgent); - } WLOG(WS_LOG_DEBUG, acceptError, "SERVER_USERAUTH_ACCEPT_DONE", ssh->error); return WS_FATAL_ERROR; } - ChannelAppend(ssh, newChannel); - newAgent->channel = newChannel->channel; - if (ssh->ctx->agentCb) { - ssh->ctx->agentCb(WOLFSSH_AGENT_LOCAL_SETUP, - ssh->agentCbCtx); - } - if (ssh->agent != NULL) - wolfSSH_AGENT_free(ssh->agent); - ssh->agent = newAgent; } #endif /* WOLFSSH_AGENT */ ssh->acceptState = ACCEPT_CLIENT_SESSION_ESTABLISHED; @@ -3937,7 +3918,8 @@ WOLFSSH_CHANNEL* wolfSSH_ChannelFwdNewRemote(WOLFSSH* ssh, if (newChannel != NULL) ChannelAppend(ssh, newChannel); - WLOG(WS_LOG_DEBUG, "Leaving wolfSSH_ChannelFwdNewRemote(), newChannel = %p, ret = %d", + WLOG(WS_LOG_DEBUG, + "Leaving wolfSSH_ChannelFwdNewRemote(), newChannel = %p, ret = %d", newChannel, ret); return newChannel; } @@ -4931,6 +4913,32 @@ int wolfSSH_CTX_SetChannelReqSubsysCb(WOLFSSH_CTX* ctx, } +int wolfSSH_CTX_SetAppChannels(WOLFSSH_CTX* ctx, byte enable) +{ + int ret = WS_SSH_CTX_NULL_E; + + if (ctx != NULL) { + ctx->appChannels = (enable != 0); + ret = WS_SUCCESS; + } + + return ret; +} + + +int wolfSSH_SetAppChannels(WOLFSSH* ssh, byte enable) +{ + int ret = WS_SSH_NULL_E; + + if (ssh != NULL) { + ssh->appChannels = (enable != 0); + ret = WS_SUCCESS; + } + + return ret; +} + + int wolfSSH_SetChannelOpenCtx(WOLFSSH* ssh, void* ctx) { int ret = WS_SSH_NULL_E; diff --git a/src/wolfscp.c b/src/wolfscp.c index 184d057cf..e5d95a3ea 100644 --- a/src/wolfscp.c +++ b/src/wolfscp.c @@ -883,6 +883,40 @@ int DoScpSource(WOLFSSH* ssh) return ret; } +/* Contract is in wolfssh/wolfscp.h. */ +int wolfSSH_SCP_accept(WOLFSSH* ssh) +{ + int ret; + + if (ssh == NULL) + return WS_BAD_ARGUMENT; + + /* Clear a want left by the previous call so the retry starts clean, + * the way the other re-entrant entry points do. */ + if (ssh->error == WS_WANT_READ || ssh->error == WS_WANT_WRITE) + ssh->error = WS_SUCCESS; + + ret = DoScpRequest(ssh); + + if (ret >= WS_SUCCESS) { + /* The tail of DoScpRequest() passes a read count through, so treat + * anything non-negative as done the way wolfSSH_accept() does. */ + ret = WS_SCP_COMPLETE; + } + else { + /* A non-blocking want on a read path surfaces as a generic error + * with the want recorded in ssh->error (see GetInputData), so + * report it as the want the caller is told to retry on. */ + int err = wolfSSH_get_error(ssh); + + if (err == WS_WANT_READ || err == WS_WANT_WRITE) + ret = err; + } + + return ret; +} + + int DoScpRequest(WOLFSSH* ssh) { int ret = WS_SUCCESS; diff --git a/src/wolfsftp.c b/src/wolfsftp.c index 88cca98f8..1b7d93cf1 100644 --- a/src/wolfsftp.c +++ b/src/wolfsftp.c @@ -1383,8 +1383,12 @@ int wolfSSH_SFTP_accept(WOLFSSH* ssh) if (ssh->error == WS_WANT_READ || ssh->error == WS_WANT_WRITE) ssh->error = WS_SUCCESS; - /* check accept is done, if not call wolfSSH accept */ - if (ssh->acceptState < ACCEPT_CLIENT_SESSION_ESTABLISHED) { + /* check accept is done, if not call wolfSSH accept. In + * application-driven mode accept() parks at ACCEPT_SERVER_USERAUTH_SENT + * and never advances, so that state counts as done here. */ + if (ssh->acceptState < ACCEPT_CLIENT_SESSION_ESTABLISHED + && !(ssh->appChannels + && ssh->acceptState >= ACCEPT_SERVER_USERAUTH_SENT)) { byte name[] = "sftp"; WLOG(WS_LOG_SFTP, "Trying to do SSH accept first"); diff --git a/tests/api.c b/tests/api.c index a0255cdd5..c5e3f7ea6 100644 --- a/tests/api.c +++ b/tests/api.c @@ -7906,6 +7906,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"; diff --git a/tests/regress.c b/tests/regress.c index eca4fd729..868069561 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -1472,6 +1472,180 @@ static void AssertHandshakeRejectsMutatedReply(const char* keyAlgo, } #ifndef WOLFSSH_NO_RSA_SHA2_256 +/* Counts the shell requests the application-driven server answered. */ +static int appChannelsShellReqCount; + +static int AppChannelsShellCb(WOLFSSH_CHANNEL* channel, void* ctx) +{ + (void)channel; + (void)ctx; + appChannelsShellReqCount++; + return 0; +} + +/* Drive an application-driven server: wolfSSH_accept() is expected to return + * at userauth, so the channel open and the shell request are answered by + * wolfSSH_worker() calls the application makes itself. */ +static void RunAppChannelsHandshake(KexReplyHarness* harness, + KexReplyRunResult* result) +{ + word32 step; + + WMEMSET(result, 0, sizeof(*result)); + result->clientRet = WS_FATAL_ERROR; + result->serverRet = WS_FATAL_ERROR; + + for (step = 0; step < REGRESS_MAX_HANDSHAKE_STEPS; step++) { + if (!result->clientSuccess) { + result->clientRet = wolfSSH_connect(harness->client); + result->clientErr = wolfSSH_get_error(harness->client); + if (result->clientRet == WS_SUCCESS) { + result->clientSuccess = 1; + } + else if (!IsHandshakeRetryable(result->clientErr)) { + result->steps = step + 1; + return; + } + } + + if (!result->serverSuccess) { + result->serverRet = wolfSSH_accept(harness->server); + result->serverErr = wolfSSH_get_error(harness->server); + if (result->serverRet == WS_SUCCESS) { + result->serverSuccess = 1; + } + else if (!IsHandshakeRetryable(result->serverErr)) { + result->steps = step + 1; + return; + } + } + else if (harness->server->clientState < CLIENT_DONE) { + result->serverRet = wolfSSH_worker(harness->server, NULL); + result->serverErr = wolfSSH_get_error(harness->server); + if (result->serverRet < WS_SUCCESS + && result->serverErr != WS_CHAN_RXD + && !IsHandshakeRetryable(result->serverErr)) { + result->steps = step + 1; + return; + } + } + + if (result->clientSuccess && result->serverSuccess + && harness->server->clientState >= CLIENT_DONE) { + result->steps = step + 1; + return; + } + } + + result->steps = REGRESS_MAX_HANDSHAKE_STEPS; +} + +/* With wolfSSH_SetAppChannels() on, accept() stops once the user is + * authenticated and the shell request lands on the callback instead. */ +static void TestAppChannelsAcceptStopsAtUserAuth(void) +{ + KexReplyHarness harness; + KexReplyRunResult result; + + appChannelsShellReqCount = 0; + + InitKexReplyHarness(&harness, "rsa-sha2-256", REGRESS_SERVER_KEY_PATH, + 0, NULL); + AssertIntEQ(wolfSSH_CTX_SetChannelReqShellCb(harness.serverCtx, + AppChannelsShellCb), WS_SUCCESS); + AssertIntEQ(wolfSSH_SetAppChannels(harness.server, 1), WS_SUCCESS); + + RunAppChannelsHandshake(&harness, &result); + + AssertTrue(result.clientSuccess); + AssertTrue(result.serverSuccess); + AssertIntEQ(harness.server->acceptState, ACCEPT_SERVER_USERAUTH_SENT); + AssertIntEQ(harness.server->clientState, CLIENT_DONE); + AssertIntEQ(appChannelsShellReqCount, 1); + AssertIntEQ(harness.client->connectState, + CONNECT_SERVER_CHANNEL_REQUEST_DONE); + AssertFalse(harness.clientIo.sawDisconnect); + AssertFalse(harness.serverIo.sawDisconnect); + + FreeKexReplyHarness(&harness); +} + +/* Same mode, no callback registered: nothing can start the shell once + * accept() has returned, so the request is refused. The default mode + * accepts it, which AssertHandshakeSucceeds() covers. */ +static void TestAppChannelsNoShellCbRejects(void) +{ + KexReplyHarness harness; + KexReplyRunResult result; + + InitKexReplyHarness(&harness, "rsa-sha2-256", REGRESS_SERVER_KEY_PATH, + 0, NULL); + AssertIntEQ(wolfSSH_SetAppChannels(harness.server, 1), WS_SUCCESS); + + RunAppChannelsHandshake(&harness, &result); + + AssertFalse(result.clientSuccess); + AssertTrue(harness.client->connectState < + CONNECT_SERVER_CHANNEL_REQUEST_DONE); + AssertIntEQ(harness.server->acceptState, ACCEPT_SERVER_USERAUTH_SENT); + + FreeKexReplyHarness(&harness); +} + +/* The flag is documented as a context setting first, so pin the setter + * returns and the inheritance wolfSSH_new() does. */ +static void TestAppChannelsCtxInherits(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + + AssertIntEQ(wolfSSH_CTX_SetAppChannels(NULL, 1), WS_SSH_CTX_NULL_E); + AssertIntEQ(wolfSSH_SetAppChannels(NULL, 1), WS_SSH_NULL_E); + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctx); + + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AssertIntEQ(ssh->appChannels, 0); + wolfSSH_free(ssh); + + AssertIntEQ(wolfSSH_CTX_SetAppChannels(ctx, 1), WS_SUCCESS); + + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AssertIntEQ(ssh->appChannels, 1); + AssertIntEQ(wolfSSH_SetAppChannels(ssh, 0), WS_SUCCESS); + AssertIntEQ(ssh->appChannels, 0); + wolfSSH_free(ssh); + + wolfSSH_CTX_free(ctx); +} + +/* Turning the mode on after accept() established the session must not leave + * the accept loop hunting for a state it has already stepped past. */ +static void TestAppChannelsLateEnableReturns(void) +{ + KexReplyHarness harness; + KexReplyRunResult result; + + InitKexReplyHarness(&harness, "rsa-sha2-256", REGRESS_SERVER_KEY_PATH, + 0, NULL); + + RunKexReplyHandshake(&harness, &result); + + AssertTrue(result.serverSuccess); + AssertIntEQ(harness.server->acceptState, + ACCEPT_CLIENT_SESSION_ESTABLISHED); + + AssertIntEQ(wolfSSH_SetAppChannels(harness.server, 1), WS_SUCCESS); + AssertIntEQ(wolfSSH_accept(harness.server), WS_SUCCESS); + AssertIntEQ(harness.server->acceptState, + ACCEPT_CLIENT_SESSION_ESTABLISHED); + + FreeKexReplyHarness(&harness); +} + static void TestKexDhReplyRejectsRsaSha2_256SigNameDowngrade(void) { AssertHandshakeSucceeds("rsa-sha2-256", REGRESS_SERVER_KEY_PATH); @@ -12302,6 +12476,10 @@ int main(int argc, char** argv) #ifdef KEXDH_REPLY_REGRESS_KEX_ALGO #ifndef WOLFSSH_NO_RSA_SHA2_256 + TestAppChannelsCtxInherits(); + TestAppChannelsAcceptStopsAtUserAuth(); + TestAppChannelsNoShellCbRejects(); + TestAppChannelsLateEnableReturns(); TestKexDhReplyRejectsRsaSha2_256SigNameDowngrade(); #endif #ifndef WOLFSSH_NO_RSA_SHA2_512 diff --git a/tests/unit.c b/tests/unit.c index 1007afcae..a136ac166 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -9245,6 +9245,68 @@ static int test_DoChannelRequest(void) } #endif /* WOLFSSH_SHELL && WOLFSSH_TERM */ + /* Application-driven channels flip the no-callback default: with + * accept() already returned there is nothing left to start a shell, + * exec or subsystem, so all three are refused rather than accepted. */ + { + static const byte paySubsys[] = { + 0x00,0x00,0x00,0x00, /* channelId = 0 */ + 0x00,0x00,0x00,0x09, /* typeSz = 9 */ + 0x73,0x75,0x62,0x73,0x79,0x73, + 0x74,0x65,0x6D, /* "subsystem" */ + 0x01, /* wantReply = 1 */ + 0x00,0x00,0x00,0x04, /* nameSz = 4 */ + 0x73,0x66,0x74,0x70 /* "sftp" */ + }; + struct { + const char* label; + const byte* payload; + word32 payloadSz; + int errBase; + } appCases[] = { + { "shell", payShell, (word32)sizeof(payShell), -495 }, + { "exec", payExec, (word32)sizeof(payExec), -497 }, + { "subsystem", paySubsys, (word32)sizeof(paySubsys), -499 } + }; + int a; + + for (a = 0; a < (int)(sizeof(appCases) / sizeof(appCases[0])); a++) { + word32 idxApp = 0; + int retApp, capMsgId; + + if (wolfSSH_SetAppChannels(ssh, 1) != WS_SUCCESS) { + printf("DoChannelRequest[app-%s]: set failed\n", + appCases[a].label); + result = appCases[a].errBase; + goto done; + } + + s_chanReqCaptureSz = 0; + WMEMSET(s_chanReqCapture, 0, sizeof(s_chanReqCapture)); + + retApp = wolfSSH_TestDoChannelRequest(ssh, + (byte*)appCases[a].payload, appCases[a].payloadSz, + &idxApp); + wolfSSH_SetAppChannels(ssh, 0); + + if (retApp != WS_SUCCESS) { + printf("DoChannelRequest[app-%s]: ret=%d, expected=%d\n", + appCases[a].label, retApp, WS_SUCCESS); + result = appCases[a].errBase; + goto done; + } + + capMsgId = CaptureMsgId(s_chanReqCapture, s_chanReqCaptureSz); + if (capMsgId != (int)MSGID_CHANNEL_FAILURE) { + printf("DoChannelRequest[app-%s]: msg_id=0x%02x, " + "expected=0x%02x\n", appCases[a].label, capMsgId, + MSGID_CHANNEL_FAILURE); + result = appCases[a].errBase - 1; + goto done; + } + } + } + done: wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); diff --git a/wolfssh/agent.h b/wolfssh/agent.h index 581e3eba9..53e00ae60 100644 --- a/wolfssh/agent.h +++ b/wolfssh/agent.h @@ -181,6 +181,14 @@ WOLFSSH_API int wolfSSH_CTX_set_agent_cb(WOLFSSH_CTX* ctx, WOLFSSH_API int wolfSSH_set_agent_cb_ctx(WOLFSSH* ssh, void* ctx); WOLFSSH_API int wolfSSH_CTX_AGENT_enable(WOLFSSH_CTX* ctx, byte isEnabled); WOLFSSH_API int wolfSSH_AGENT_enable(WOLFSSH* ssh, byte isEnabled); +/* Server side. Opens the auth-agent@openssh.com channel back to the client + * once an auth-agent-req@openssh.com request has set the session up for it. + * wolfSSH_accept() does this itself on the default path; an application that + * drives its own channels returns from accept() before that point and calls + * this instead. Idempotent, so it is safe to poll while waiting for the + * peer's request. Returns WS_SUCCESS, or WS_BAD_ARGUMENT when the session + * never asked for agent forwarding. */ +WOLFSSH_API int wolfSSH_AGENT_ChannelOpen(WOLFSSH* ssh); WOLFSSH_LOCAL int wolfSSH_AGENT_worker(WOLFSSH* ssh); WOLFSSH_API int wolfSSH_AGENT_Relay(WOLFSSH* ssh, const byte* msg, word32* msgSz, byte* rsp, word32* rspSz); diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 588e0c867..468e81dee 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -867,6 +867,7 @@ struct WOLFSSH_CTX { word32 maxAuthAttempts; /* server cap on failed userauth */ byte side; /* client or server */ byte showBanner; + byte appChannels; /* app drives channels, see ssh.h */ #ifdef WOLFSSH_AGENT byte agentEnabled; #endif /* WOLFSSH_AGENT */ @@ -1136,6 +1137,7 @@ struct WOLFSSH { byte serverState; byte processReplyState; byte isKeying; + byte appChannels; /* app drives channels, see ssh.h */ byte authId; /* if using public key or password */ byte supportedAuth[4]; /* supported auth IDs public key , password */ diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 545ef9e3c..8a943bc22 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -452,6 +452,29 @@ WOLFSSH_API int wolfSSH_CTX_SetChannelReqSubsysCb(WOLFSSH_CTX* ctx, WOLFSSH_API int wolfSSH_SetChannelReqCtx(WOLFSSH* ssh, void* ctx); WOLFSSH_API void* wolfSSH_GetChannelReqCtx(WOLFSSH* ssh); +/* Application-driven channel handling, server side, off by default. + * + * Off, wolfSSH_accept() runs the session state machine through to an + * established session with the first channel open, as it always has, and a + * shell, exec, or subsystem request with no callback registered for it is + * accepted. + * + * On, wolfSSH_accept() returns WS_SUCCESS as soon as the user has + * authenticated, and the application owns every channel from there, driving + * the session with wolfSSH_worker() and the callbacks above. A shell, exec, + * or subsystem request with no callback registered is then rejected: with + * accept() already returned, nothing is left to service it. + * + * Set it on the context before wolfSSH_new(), or on a session before the + * first wolfSSH_accept() call. Turning it on once accept() has established + * the session has no effect on that session. + * + * The mode drives the session channels itself, so it does not combine with + * the built-in wolfSSH_SFTP_accept() and WS_SCP_INIT entry points; an + * application using those leaves this off. */ +WOLFSSH_API int wolfSSH_CTX_SetAppChannels(WOLFSSH_CTX* ctx, byte enable); +WOLFSSH_API int wolfSSH_SetAppChannels(WOLFSSH* ssh, byte enable); + typedef int (*WS_CallbackChannelEof)(WOLFSSH_CHANNEL* channel, void* ctx); WOLFSSH_API int wolfSSH_CTX_SetChannelEofCb(WOLFSSH_CTX* ctx, WS_CallbackChannelEof cb); diff --git a/wolfssh/wolfscp.h b/wolfssh/wolfscp.h index 32e9dc7de..1ebeb339f 100644 --- a/wolfssh/wolfscp.h +++ b/wolfssh/wolfscp.h @@ -158,6 +158,17 @@ WOLFSSH_API int wolfSSH_SCP_to(WOLFSSH* ssh, const char* src, const char* dst); WOLFSSH_API int wolfSSH_SCP_from(WOLFSSH* ssh, const char* src, const char* dst); +/* Server side. Drives an SCP transfer on a channel whose "exec scp ..." + * command is already bound. This is the same work wolfSSH_accept() does + * through its WS_SCP_INIT re-entry, exposed so an application can start the + * transfer itself; use one or the other, not both. Call it once + * wolfSSH_accept() has returned and the exec channel-request callback has + * reported an SCP command, not from inside that callback. + * + * Returns WS_SCP_COMPLETE when the transfer is done. On a non-blocking + * socket it returns WS_WANT_READ or WS_WANT_WRITE with the transfer part + * done; call it again on the same session until it completes. */ +WOLFSSH_API int wolfSSH_SCP_accept(WOLFSSH* ssh); #ifdef __cplusplus