diff --git a/src/agent.c b/src/agent.c index 33c0eb936..ba8618112 100644 --- a/src/agent.c +++ b/src/agent.c @@ -1731,6 +1731,91 @@ 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; + /* wolfSSH_accept() clears only want-read/want-write/auth-pending, so a + * WS_BAD_ARGUMENT latched by a poll kills the handshake. */ + int recordError = 0; + + WLOG_ENTER(); + + if (ssh == NULL) + ret = WS_SSH_NULL_E; + else if (ssh->ctx->side != WOLFSSH_ENDPOINT_SERVER) { + /* Server side only. wolfSSH_connect() sets ssh->agent too, so the + * checks below would report a channel a client never opened. */ + ret = WS_BAD_ARGUMENT; + } + else if (SendAfterDisconnect(ssh)) { + /* The session is over, so neither a new open nor the flush of one + * queued before the disconnect may go out. RFC 4253 section 11.1. + * WS_DISCONNECT is in ssh->error, where the rest of the API puts + * it. */ + ret = WS_FATAL_ERROR; + } + else if (!ssh->useAgent) { + /* Nothing asked for agent forwarding on this session. */ + ret = WS_BAD_ARGUMENT; + } + else if (ssh->agent == NULL) { + /* Nothing else sets ssh->agent, so a NULL one means "not opened + * yet". Idempotent, so a poll cannot open a second channel. */ + 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) { + recordError = 1; + ret = SendChannelOpenSession(ssh, newChannel); + + if (ret < WS_SUCCESS + && ret != WS_WANT_WRITE && ret != WS_WANT_READ) { + ChannelDelete(newChannel, ssh->ctx->heap); + } + else { + /* Publish on a queued open too, so a retry takes the + * already-open path rather than opening a second. */ + 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); + } + else if (wolfSSH_OutputPending(ssh)) { + /* Any queued output, not just this open. Flush it rather than + * report a success the peer hasn't seen. */ + recordError = 1; + ret = wolfSSH_SendPacket(ssh); + } + + if (recordError) + 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 182c43df4..743307d50 100644 --- a/src/internal.c +++ b/src/internal.c @@ -1672,6 +1672,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; @@ -11799,6 +11800,56 @@ static int DoGlobalRequestFwd(WOLFSSH* ssh, } #endif +/* Puts a global request to the generic callback, which sees the name and + * the type-specific part to parse itself. Returns 1 when the callback + * settled the request, with any wanted reply sent and *ret carrying the + * result, or 0 to leave it to the built-in handling. */ +static int DoGlobalRequestAny(WOLFSSH* ssh, const char* name, int globReqId, + byte* buf, word32 len, word32 begin, byte wantReply, int* ret) +{ + int decision, success; + + if (ssh->ctx->globalReqAnyCb == NULL) { + return 0; + } + + decision = ssh->ctx->globalReqAnyCb(ssh, name, buf + begin, len - begin, + wantReply, ssh->globalReqCtx); + if (decision != WOLFSSH_REQ_ACCEPT && decision != WOLFSSH_REQ_REJECT) { + return 0; + } + success = (decision == WOLFSSH_REQ_ACCEPT); + +#ifdef WOLFSSH_FWD + /* RFC 4254 7.1: a port-0 request is answered with the port bound, + * which only the forward callback can report. */ + if (success && globReqId == ID_GLOBREQ_TCPIP_FWD) { + const byte* bindAddr; + word32 bindAddrSz, bindPort = 0, peek = begin; + + if (GetStringRef(&bindAddrSz, &bindAddr, buf, len, &peek) + != WS_SUCCESS + || GetUint32(&bindPort, buf, len, &peek) != WS_SUCCESS + || bindPort == 0) { + WLOG(WS_LOG_WARN, "DGR: a port-0 forward needs the forward " + "callback to bind it; rejecting"); + success = 0; + } + } +#else + (void)globReqId; +#endif + + WLOG(WS_LOG_DEBUG, "DGR: global request callback %s", + success ? "granted" : "refused"); + if (wantReply) { + *ret = SendRequestSuccess(ssh, success); + } + + return 1; +} + + static int DoGlobalRequest(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) { @@ -11845,31 +11896,37 @@ static int DoGlobalRequest(WOLFSSH* ssh, } else #endif - switch (globReqId) { + if (!DoGlobalRequestAny(ssh, name, globReqId, buf, len, begin, + wantReply, &ret)) { + switch (globReqId) { #ifdef WOLFSSH_FWD - case ID_GLOBREQ_TCPIP_FWD: - ret = DoGlobalRequestFwd(ssh, buf, len, &begin, wantReply, 0); - wantReply = 0; - break; - case ID_GLOBREQ_TCPIP_FWD_CANCEL: - ret = DoGlobalRequestFwd(ssh, buf, len, &begin, wantReply, 1); - wantReply = 0; - break; + case ID_GLOBREQ_TCPIP_FWD: + ret = DoGlobalRequestFwd(ssh, buf, len, &begin, + wantReply, 0); + wantReply = 0; + break; + case ID_GLOBREQ_TCPIP_FWD_CANCEL: + ret = DoGlobalRequestFwd(ssh, buf, len, &begin, + wantReply, 1); + wantReply = 0; + break; #endif - default: - if (ssh->ctx->globalReqCb != NULL) { - ret = ssh->ctx->globalReqCb(ssh, name, nameSz, wantReply, - (void *)ssh->globalReqCtx); + default: + if (ssh->ctx->globalReqCb != NULL) { + ret = ssh->ctx->globalReqCb(ssh, name, nameSz, + wantReply, (void *)ssh->globalReqCtx); - if (wantReply) { - ret = SendRequestSuccess(ssh, (ret == WS_SUCCESS)); + if (wantReply) { + ret = SendRequestSuccess(ssh, + (ret == WS_SUCCESS)); + } } - } - else if (wantReply) - ret = SendRequestSuccess(ssh, 0); - /* response SSH_MSG_REQUEST_FAILURE to Keep-Alive. - * IETF:draft-ssh-global-requests */ - break; + else if (wantReply) + ret = SendRequestSuccess(ssh, 0); + /* response SSH_MSG_REQUEST_FAILURE to Keep-Alive. + * IETF:draft-ssh-global-requests */ + break; + } } } @@ -12666,6 +12723,65 @@ static void SetTerminalSize(WOLFSSH* ssh, word32 widthChar, word32 heightRows, #endif /* WOLFSSH_TERM */ +/* Answers a shell, exec, or subsystem request. The session type, and the + * command for the two that carry one, are set for the callback to read and + * kept only if it accepts; a refused request leaves the channel as it was + * and the accept loop still waiting, so nothing serves a session the + * application turned down. Without a callback the request is accepted, + * unless the application drives its own channels. A request the generic + * callback already granted asks no callback. */ +static int DoChannelRequestSession(WOLFSSH* ssh, WOLFSSH_CHANNEL* channel, + byte sessionType, WS_CallbackChannelReq cb, int granted, + byte* buf, word32 len, word32* idx, int* rej) +{ + char* prevCommand = NULL; + byte prevType = channel->sessionType; + byte hasCommand = (sessionType != WOLFSSH_SESSION_SHELL); + int ret = WS_SUCCESS; + + if (hasCommand) { + prevCommand = channel->command; + channel->command = NULL; + ret = GetStringAlloc(ssh->ctx->heap, &channel->command, NULL, + buf, len, idx); + if (ret == WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, " command = %s", channel->command); + } + } + + if (ret == WS_SUCCESS) { + channel->sessionType = sessionType; + if (granted) { + *rej = 0; + } + else if (cb != NULL) { + *rej = cb(channel, ssh->channelReqCtx); + } + else { + *rej = ssh->appChannels; + } + } + + if (ret == WS_SUCCESS && !*rej) { + if (prevCommand != NULL) { + WFREE(prevCommand, ssh->ctx->heap, DYNTYPE_STRING); + } + ssh->clientState = CLIENT_DONE; + } + else { + if (hasCommand) { + if (channel->command != NULL) { + WFREE(channel->command, ssh->ctx->heap, DYNTYPE_STRING); + } + channel->command = prevCommand; + } + channel->sessionType = prevType; + } + + return ret; +} + + static int DoChannelRequest(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) { @@ -12675,7 +12791,7 @@ static int DoChannelRequest(WOLFSSH* ssh, word32 typeSz; char type[32]; byte wantReply; - int ret, rej = 0; + int ret, rej = 0, granted = 0; WLOG(WS_LOG_DEBUG, "Entering DoChannelRequest()"); @@ -12704,6 +12820,23 @@ static int DoChannelRequest(WOLFSSH* ssh, WLOG(WS_LOG_DEBUG, " type = %s", type); WLOG(WS_LOG_DEBUG, " wantReply = %u", wantReply); + /* The generic callback sees every request first, with the + * type-specific part to parse itself. A refusal skips the handling + * below; a grant runs it with the decision already made. */ + if (ssh->ctx->channelReqAnyCb != NULL) { + int decision = ssh->ctx->channelReqAnyCb(channel, type, + buf + begin, len - begin, ssh->channelReqCtx); + if (decision == WOLFSSH_REQ_REJECT) { + WLOG(WS_LOG_DEBUG, " channel request callback refused."); + rej = 1; + } + else if (decision == WOLFSSH_REQ_ACCEPT) { + granted = 1; + } + } + } + + if (ret == WS_SUCCESS && !rej) { if (ChannelRequestIs(type, typeSz, "env")) { char name[WOLFSSH_MAX_NAMESZ]; word32 nameSz; @@ -12721,33 +12854,19 @@ static int DoChannelRequest(WOLFSSH* ssh, WLOG(WS_LOG_DEBUG, " %s = %s", name, value); } else if (ChannelRequestIs(type, typeSz, "shell")) { - channel->sessionType = WOLFSSH_SESSION_SHELL; - if (ssh->ctx->channelReqShellCb) { - rej = ssh->ctx->channelReqShellCb(channel, ssh->channelReqCtx); - } - ssh->clientState = CLIENT_DONE; + ret = DoChannelRequestSession(ssh, channel, WOLFSSH_SESSION_SHELL, + ssh->ctx->channelReqShellCb, granted, buf, len, &begin, + &rej); } else if (ChannelRequestIs(type, typeSz, "exec")) { - ret = GetStringAlloc(ssh->ctx->heap, &channel->command, NULL, - buf, len, &begin); - channel->sessionType = WOLFSSH_SESSION_EXEC; - if (ssh->ctx->channelReqExecCb) { - rej = ssh->ctx->channelReqExecCb(channel, ssh->channelReqCtx); - } - ssh->clientState = CLIENT_DONE; - - WLOG(WS_LOG_DEBUG, " command = %s", channel->command); + ret = DoChannelRequestSession(ssh, channel, WOLFSSH_SESSION_EXEC, + ssh->ctx->channelReqExecCb, granted, buf, len, &begin, + &rej); } else if (ChannelRequestIs(type, typeSz, "subsystem")) { - ret = GetStringAlloc(ssh->ctx->heap, &channel->command, NULL, - buf, len, &begin); - channel->sessionType = WOLFSSH_SESSION_SUBSYSTEM; - if (ssh->ctx->channelReqSubsysCb) { - rej = ssh->ctx->channelReqSubsysCb(channel, ssh->channelReqCtx); - } - ssh->clientState = CLIENT_DONE; - - WLOG(WS_LOG_DEBUG, " subsystem = %s", channel->command); + ret = DoChannelRequestSession(ssh, channel, + WOLFSSH_SESSION_SUBSYSTEM, ssh->ctx->channelReqSubsysCb, + granted, buf, len, &begin, &rej); } #ifdef WOLFSSH_TERM else if (ChannelRequestIs(type, typeSz, "pty-req")) { @@ -12872,6 +12991,9 @@ static int DoChannelRequest(WOLFSSH* ssh, WLOG(WS_LOG_AGENT, "Agent callback not set, not using."); } #endif /* WOLFSSH_AGENT */ + else if (granted) { + WLOG(WS_LOG_DEBUG, " unknown channel request type, granted."); + } else { WLOG(WS_LOG_DEBUG, " unknown channel request type, rejecting."); rej = 1; @@ -12886,7 +13008,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 c02609769..869ab1499 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -345,6 +345,19 @@ void wolfSSH_SetReqFailure(WOLFSSH_CTX *ctx, WS_CallbackReqSuccess cb) ctx->reqFailureCb = cb; } +int wolfSSH_CTX_SetGlobalReqCb(WOLFSSH_CTX* ctx, WS_CallbackGlobalReqAny cb) +{ + int ret = WS_SSH_CTX_NULL_E; + + if (ctx != NULL) { + ctx->globalReqAnyCb = cb; + ret = WS_SUCCESS; + } + + return ret; +} + + void wolfSSH_SetGlobalReqCtx(WOLFSSH* ssh, void *ctx) { WLOG(WS_LOG_DEBUG, "Entering wolfSSH_SetGlobalReqCtx()"); @@ -567,10 +580,6 @@ static int DoReceiveHandshake(WOLFSSH* ssh) #endif /* !NO_WOLFSSH_SERVER || !NO_WOLFSSH_CLIENT */ -/* Defined below, ahead of both drivers; either can be the only one built. */ -static int SendAfterDisconnect(WOLFSSH* ssh); - - #ifndef NO_WOLFSSH_SERVER const char acceptError[] = "accept error: %s, %d"; @@ -579,6 +588,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 +609,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 +629,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 +655,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 +745,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 +794,17 @@ 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; - } + int agentRet = wolfSSH_AGENT_ChannelOpen(ssh); - 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; + if (agentRet < WS_SUCCESS) { + /* WS_FATAL_ERROR is the disconnect, which already + * recorded WS_DISCONNECT; keep that. */ + if (agentRet != WS_FATAL_ERROR) + ssh->error = agentRet; WLOG(WS_LOG_DEBUG, acceptError, "SERVER_USERAUTH_ACCEPT_DONE", ssh->error); return WS_FATAL_ERROR; } - - ssh->error = SendChannelOpenSession(ssh, newChannel); - 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; @@ -1134,11 +1129,8 @@ int wolfSSH_connect(WOLFSSH* ssh) #endif /* NO_WOLFSSH_CLIENT */ -/* A disconnect, sent or received, ends the session, so nothing further may - * go out. RFC 4253 section 11.1. Reads are deliberately not gated on this: - * channel data that arrived before the disconnect is still the caller's. - * Call only after ssh has been checked for NULL. */ -static int SendAfterDisconnect(WOLFSSH* ssh) +/* See wolfssh/internal.h for the contract. */ +int SendAfterDisconnect(WOLFSSH* ssh) { if (ssh->disconnected) { WLOG(WS_LOG_DEBUG, "Send attempted after a disconnect"); @@ -3963,7 +3955,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; } @@ -4957,6 +4950,46 @@ int wolfSSH_CTX_SetChannelReqSubsysCb(WOLFSSH_CTX* ctx, } +int wolfSSH_CTX_SetChannelReqCb(WOLFSSH_CTX* ctx, + WS_CallbackChannelReqAny cb) +{ + int ret = WS_SSH_CTX_NULL_E; + + if (ctx != NULL) { + ctx->channelReqAnyCb = cb; + ret = WS_SUCCESS; + } + + return ret; +} + + +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/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/regress.c b/tests/regress.c index c87c888d7..3a6efce55 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -1486,6 +1486,255 @@ 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. The flag + * still reaches DoChannelRequest() from there, which is what ssh.h promises, + * so pin both halves: accept() stays put, the requests that follow flip. */ +static void TestAppChannelsLateEnableReturns(void) +{ + KexReplyHarness harness; + KexReplyRunResult result; + /* SSH_MSG_CHANNEL_REQUEST body: channel 0, "shell", wantReply. */ + static byte payShell[] = { + 0x00,0x00,0x00,0x00, /* channelId = 0 */ + 0x00,0x00,0x00,0x05, /* typeSz = 5 */ + 0x73,0x68,0x65,0x6C,0x6C, /* "shell" */ + 0x01 /* wantReply = 1 */ + }; + word32 idx; + + 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); + + /* Default mode, no callback registered: the request is granted. */ + idx = 0; + AssertIntEQ(wolfSSH_TestDoChannelRequest(harness.server, payShell, + (word32)sizeof(payShell), &idx), WS_SUCCESS); + AssertIntEQ(wolfSSH_worker(harness.client, NULL), WS_SUCCESS); + + AssertIntEQ(wolfSSH_SetAppChannels(harness.server, 1), WS_SUCCESS); + AssertIntEQ(wolfSSH_accept(harness.server), WS_SUCCESS); + AssertIntEQ(harness.server->acceptState, + ACCEPT_CLIENT_SESSION_ESTABLISHED); + + /* Same request, same session, mode now on: refused instead. */ + idx = 0; + AssertIntEQ(wolfSSH_TestDoChannelRequest(harness.server, payShell, + (word32)sizeof(payShell), &idx), WS_SUCCESS); + AssertTrue(wolfSSH_worker(harness.client, NULL) < WS_SUCCESS); + AssertIntEQ(wolfSSH_get_error(harness.client), WS_CHANOPEN_FAILED); + + FreeKexReplyHarness(&harness); +} + +/* Refuses the session request, and records what the channel showed. */ +static int rejectShellReqCalls; +static WS_SessionType rejectShellReqType; + +static int RejectShellReqCb(WOLFSSH_CHANNEL* channel, void* ctx) +{ + (void)ctx; + rejectShellReqCalls++; + rejectShellReqType = wolfSSH_ChannelGetSessionType(channel); + return 1; +} + +/* A shell request the callback refuses gets CHANNEL_FAILURE and nothing + * more: the channel keeps no session type, and accept() stays where it was, + * waiting on a request it can grant, rather than reporting an established + * session it just refused. */ +static void TestSessionReqRejectedKeepsAcceptWaiting(void) +{ + KexReplyHarness harness; + KexReplyRunResult result; + WOLFSSH_CHANNEL* channel; + WS_SessionType sessionType; + + rejectShellReqCalls = 0; + rejectShellReqType = WOLFSSH_SESSION_UNKNOWN; + + InitKexReplyHarness(&harness, "rsa-sha2-256", REGRESS_SERVER_KEY_PATH, + 0, NULL); + AssertIntEQ(wolfSSH_CTX_SetChannelReqShellCb(harness.serverCtx, + RejectShellReqCb), WS_SUCCESS); + + RunKexReplyHandshake(&harness, &result); + + AssertIntEQ(rejectShellReqCalls, 1); + AssertIntEQ(rejectShellReqType, WOLFSSH_SESSION_SHELL); + AssertFalse(result.clientSuccess); + AssertIntEQ(result.clientErr, WS_CHANOPEN_FAILED); + AssertFalse(result.serverSuccess); + AssertIntEQ(harness.server->acceptState, + ACCEPT_SERVER_CHANNEL_ACCEPT_SENT); + AssertTrue(harness.server->clientState < CLIENT_DONE); + sessionType = wolfSSH_GetSessionType(harness.server); + AssertIntEQ(sessionType, WOLFSSH_SESSION_UNKNOWN); + channel = wolfSSH_ChannelNext(harness.server, NULL); + AssertNotNull(channel); + AssertIntEQ(channel->sessionType, WOLFSSH_SESSION_UNKNOWN); + AssertFalse(harness.clientIo.sawDisconnect); + AssertFalse(harness.serverIo.sawDisconnect); + + FreeKexReplyHarness(&harness); +} + static void TestKexDhReplyRejectsRsaSha2_256SigNameDowngrade(void) { AssertHandshakeSucceeds("rsa-sha2-256", REGRESS_SERVER_KEY_PATH); @@ -3530,6 +3779,379 @@ static void TestChannelReqSubsysCallbackRuns(void) WOLFSSH_SESSION_SUBSYSTEM), MSGID_CHANNEL_FAILURE); } +/* Builds a plaintext SSH_MSG_CHANNEL_REQUEST with a raw type-specific + * tail, so a test can send any request type. */ +static word32 BuildChannelRequestPacket(word32 recipientChannelId, + const char* type, byte wantReply, const byte* tail, word32 tailSz, + byte* out, word32 outSz) +{ + byte payload[128]; + word32 idx = 0; + + idx = AppendUint32(payload, sizeof(payload), idx, recipientChannelId); + idx = AppendString(payload, sizeof(payload), idx, type); + idx = AppendByte(payload, sizeof(payload), idx, wantReply); + idx = AppendData(payload, sizeof(payload), idx, tail, tailSz); + + return WrapPacket(MSGID_CHANNEL_REQUEST, payload, idx, out, outSz); +} + +/* Builds a plaintext SSH_MSG_GLOBAL_REQUEST with a raw type-specific + * tail. */ +static word32 BuildGlobalRequestPacket(const char* name, byte wantReply, + const byte* tail, word32 tailSz, byte* out, word32 outSz) +{ + byte payload[128]; + word32 idx = 0; + + idx = AppendString(payload, sizeof(payload), idx, name); + idx = AppendByte(payload, sizeof(payload), idx, wantReply); + idx = AppendData(payload, sizeof(payload), idx, tail, tailSz); + + return WrapPacket(MSGID_GLOBAL_REQUEST, payload, idx, out, outSz); +} + +/* What the generic request callbacks saw, and what they answer. */ +static int anyReqCbCalls; +static char anyReqCbName[32]; +static byte anyReqCbData[64]; +static word32 anyReqCbDataSz; +static int anyReqCbWantReply; +static void* anyReqCbCtx; +static int anyReqCbReturn; + +static void ResetAnyReqCb(int cbReturn) +{ + anyReqCbCalls = 0; + anyReqCbName[0] = 0; + anyReqCbDataSz = 0; + anyReqCbWantReply = -1; + anyReqCbCtx = NULL; + anyReqCbReturn = cbReturn; +} + +static void RecordAnyReq(const char* name, const byte* data, word32 dataSz, + void* ctx) +{ + anyReqCbCalls++; + WSTRNCPY(anyReqCbName, name, sizeof(anyReqCbName) - 1); + anyReqCbName[sizeof(anyReqCbName) - 1] = 0; + anyReqCbDataSz = dataSz; + if (dataSz > 0) { + AssertTrue(dataSz <= sizeof(anyReqCbData)); + WMEMCPY(anyReqCbData, data, dataSz); + } + anyReqCbCtx = ctx; +} + +static int RecordingChannelReqAnyCb(WOLFSSH_CHANNEL* channel, + const char* type, const byte* data, word32 dataSz, void* ctx) +{ + AssertNotNull(channel); + RecordAnyReq(type, data, dataSz, ctx); + return anyReqCbReturn; +} + +static int RecordingGlobalReqAnyCb(WOLFSSH* ssh, const char* name, + const byte* data, word32 dataSz, int wantReply, void* ctx) +{ + AssertNotNull(ssh); + RecordAnyReq(name, data, dataSz, ctx); + anyReqCbWantReply = wantReply; + return anyReqCbReturn; +} + +/* A typed session callback that only counts, to show whether the generic + * callback left the request to it. */ +static int typedReqCbCalls; + +static int CountingSessionReqCb(WOLFSSH_CHANNEL* channel, void* ctx) +{ + (void)channel; + (void)ctx; + typedReqCbCalls++; + return 0; +} + +/* Seeds a confirmed session channel on the harness, the state a channel is + * in when requests arrive on it. */ +static WOLFSSH_CHANNEL* SeedConfirmedSessionChannel( + ChannelOpenHarness* harness) +{ + WOLFSSH_CHANNEL* channel; + + channel = ChannelNew(harness->ssh, ID_CHANTYPE_SESSION, 1024, 1024); + AssertNotNull(channel); + AssertIntEQ(ChannelAppend(harness->ssh, channel), WS_SUCCESS); + AssertIntEQ(ChannelUpdatePeer(channel, 5, 1024, 1024), WS_SUCCESS); + channel->openConfirmed = 1; + + return channel; +} + +/* Feeds one packet to the harness and returns the id of the reply, or 0 + * when nothing was sent. */ +static byte ReplyToPacket(ChannelOpenHarness* harness, byte* in, + word32 inSz) +{ + RepointHarnessInput(harness, in, inSz); + AssertIntEQ(DoReceive(harness->ssh), WS_SUCCESS); + AssertIntEQ(harness->io.inOff, harness->io.inSz); + + return harness->io.outSz == 0 ? 0 : ParseMsgId(harness->io.out, + harness->io.outSz); +} + +/* The generic channel request callback sees every request first, with + * the type-specific part to parse itself, and can refuse a type the + * library would otherwise take in. */ +static void TestChannelReqCallbackSeesRequestAndRefuses(void) +{ + ChannelOpenHarness harness; + WOLFSSH_CHANNEL* channel; + byte tail[32]; + word32 tailSz = 0; + byte in[128]; + word32 inSz; + int cbCtx = 0; + + ResetAnyReqCb(WOLFSSH_REQ_REJECT); + InitChannelOpenHarness(&harness, NULL, 0); + AssertIntEQ(wolfSSH_CTX_SetChannelReqCb(harness.ctx, + RecordingChannelReqAnyCb), WS_SUCCESS); + AssertIntEQ(wolfSSH_SetChannelReqCtx(harness.ssh, &cbCtx), WS_SUCCESS); + channel = SeedConfirmedSessionChannel(&harness); + + tailSz = AppendString(tail, sizeof(tail), tailSz, "FOO"); + tailSz = AppendString(tail, sizeof(tail), tailSz, "bar"); + inSz = BuildChannelRequestPacket(channel->channel, "env", 1, + tail, tailSz, in, sizeof(in)); + + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_CHANNEL_FAILURE); + AssertIntEQ(anyReqCbCalls, 1); + AssertIntEQ(WSTRCMP(anyReqCbName, "env"), 0); + AssertIntEQ(anyReqCbDataSz, tailSz); + AssertIntEQ(WMEMCMP(anyReqCbData, tail, tailSz), 0); + AssertTrue(anyReqCbCtx == &cbCtx); + + /* Left to the built-in handling, the same request is taken in. */ + anyReqCbReturn = WOLFSSH_REQ_UNHANDLED; + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_CHANNEL_SUCCESS); + AssertIntEQ(anyReqCbCalls, 2); + + FreeChannelOpenHarness(&harness); +} + +/* A type the library does not know is refused unless the callback grants + * it, which is how an application answers its own request types. */ +static void TestChannelReqCallbackGrantsUnknownType(void) +{ + ChannelOpenHarness harness; + WOLFSSH_CHANNEL* channel; + static const byte tail[] = { 1, 2, 3 }; + byte in[128]; + word32 inSz; + + ResetAnyReqCb(WOLFSSH_REQ_ACCEPT); + InitChannelOpenHarness(&harness, NULL, 0); + AssertIntEQ(wolfSSH_CTX_SetChannelReqCb(harness.ctx, + RecordingChannelReqAnyCb), WS_SUCCESS); + channel = SeedConfirmedSessionChannel(&harness); + + inSz = BuildChannelRequestPacket(channel->channel, "x-custom@wolfssh", + 1, tail, sizeof(tail), in, sizeof(in)); + + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_CHANNEL_SUCCESS); + AssertIntEQ(WSTRCMP(anyReqCbName, "x-custom@wolfssh"), 0); + AssertIntEQ(anyReqCbDataSz, sizeof(tail)); + + anyReqCbReturn = WOLFSSH_REQ_UNHANDLED; + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_CHANNEL_FAILURE); + + FreeChannelOpenHarness(&harness); +} + +/* Drives one exec request through a fresh harness that registers both the + * generic and the typed exec callback, the generic one set to answer + * anyReturn, and returns the reply id. The harness is left for the caller + * to inspect and free. */ +static byte RunExecThroughBothCallbacks(ChannelOpenHarness* harness, + WOLFSSH_CHANNEL** channel, int anyReturn) +{ + byte tail[32]; + word32 tailSz = 0; + byte in[128]; + word32 inSz; + + ResetAnyReqCb(anyReturn); + typedReqCbCalls = 0; + InitChannelOpenHarness(harness, NULL, 0); + AssertIntEQ(wolfSSH_CTX_SetChannelReqCb(harness->ctx, + RecordingChannelReqAnyCb), WS_SUCCESS); + AssertIntEQ(wolfSSH_CTX_SetChannelReqExecCb(harness->ctx, + CountingSessionReqCb), WS_SUCCESS); + *channel = SeedConfirmedSessionChannel(harness); + + tailSz = AppendString(tail, sizeof(tail), tailSz, "ls"); + inSz = BuildChannelRequestPacket((*channel)->channel, "exec", 1, + tail, tailSz, in, sizeof(in)); + + return ReplyToPacket(harness, in, inSz); +} + +/* A session request the generic callback settles asks the typed callback + * nothing. A grant still commits the session, since the library needs the + * type and command whoever decided; a refusal commits nothing. */ +static void TestChannelReqCallbackSettlesSessionRequest(void) +{ + ChannelOpenHarness harness; + WOLFSSH_CHANNEL* channel; + + AssertIntEQ(RunExecThroughBothCallbacks(&harness, &channel, + WOLFSSH_REQ_ACCEPT), MSGID_CHANNEL_SUCCESS); + AssertIntEQ(typedReqCbCalls, 0); + AssertIntEQ(channel->sessionType, WOLFSSH_SESSION_EXEC); + AssertNotNull(channel->command); + AssertIntEQ(WSTRCMP(channel->command, "ls"), 0); + AssertIntEQ(harness.ssh->clientState, CLIENT_DONE); + FreeChannelOpenHarness(&harness); + + AssertIntEQ(RunExecThroughBothCallbacks(&harness, &channel, + WOLFSSH_REQ_REJECT), MSGID_CHANNEL_FAILURE); + AssertIntEQ(typedReqCbCalls, 0); + AssertIntEQ(channel->sessionType, WOLFSSH_SESSION_UNKNOWN); + AssertNull(channel->command); + AssertTrue(harness.ssh->clientState < CLIENT_DONE); + FreeChannelOpenHarness(&harness); + + AssertIntEQ(RunExecThroughBothCallbacks(&harness, &channel, + WOLFSSH_REQ_UNHANDLED), MSGID_CHANNEL_SUCCESS); + AssertIntEQ(typedReqCbCalls, 1); + AssertIntEQ(channel->sessionType, WOLFSSH_SESSION_EXEC); + FreeChannelOpenHarness(&harness); +} + +/* With application-driven channels on and no shell callback, a shell + * request is refused unless the generic callback grants it. */ +static void TestChannelReqCallbackGrantOverridesAppChannels(void) +{ + ChannelOpenHarness harness; + WOLFSSH_CHANNEL* channel; + byte in[128]; + word32 inSz; + + ResetAnyReqCb(WOLFSSH_REQ_UNHANDLED); + InitChannelOpenHarness(&harness, NULL, 0); + AssertIntEQ(wolfSSH_CTX_SetChannelReqCb(harness.ctx, + RecordingChannelReqAnyCb), WS_SUCCESS); + AssertIntEQ(wolfSSH_SetAppChannels(harness.ssh, 1), WS_SUCCESS); + channel = SeedConfirmedSessionChannel(&harness); + + inSz = BuildChannelRequestPacket(channel->channel, "shell", 1, + NULL, 0, in, sizeof(in)); + + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_CHANNEL_FAILURE); + AssertIntEQ(channel->sessionType, WOLFSSH_SESSION_UNKNOWN); + + anyReqCbReturn = WOLFSSH_REQ_ACCEPT; + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_CHANNEL_SUCCESS); + AssertIntEQ(channel->sessionType, WOLFSSH_SESSION_SHELL); + AssertIntEQ(harness.ssh->clientState, CLIENT_DONE); + + FreeChannelOpenHarness(&harness); +} + +/* The generic global request callback sees the name, the type-specific + * part and whether a reply is wanted, and its answer is the reply. Left + * unhandled, a name nothing else answers is refused as before. */ +static void TestGlobalReqCallbackSettlesRequest(void) +{ + ChannelOpenHarness harness; + static const byte tail[] = { 7, 8 }; + byte in[128]; + word32 inSz; + int cbCtx = 0; + + ResetAnyReqCb(WOLFSSH_REQ_ACCEPT); + InitChannelOpenHarness(&harness, NULL, 0); + AssertIntEQ(wolfSSH_CTX_SetGlobalReqCb(harness.ctx, + RecordingGlobalReqAnyCb), WS_SUCCESS); + wolfSSH_SetGlobalReqCtx(harness.ssh, &cbCtx); + + inSz = BuildGlobalRequestPacket("keepalive@openssh.com", 1, + tail, sizeof(tail), in, sizeof(in)); + + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_REQUEST_SUCCESS); + AssertIntEQ(anyReqCbCalls, 1); + AssertIntEQ(WSTRCMP(anyReqCbName, "keepalive@openssh.com"), 0); + AssertIntEQ(anyReqCbDataSz, sizeof(tail)); + AssertIntEQ(WMEMCMP(anyReqCbData, tail, sizeof(tail)), 0); + AssertIntEQ(anyReqCbWantReply, 1); + AssertTrue(anyReqCbCtx == &cbCtx); + + anyReqCbReturn = WOLFSSH_REQ_REJECT; + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_REQUEST_FAILURE); + + anyReqCbReturn = WOLFSSH_REQ_UNHANDLED; + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_REQUEST_FAILURE); + + /* No reply wanted, none sent, whatever the answer. */ + anyReqCbReturn = WOLFSSH_REQ_REJECT; + inSz = BuildGlobalRequestPacket("keepalive@openssh.com", 0, + tail, sizeof(tail), in, sizeof(in)); + AssertIntEQ(ReplyToPacket(&harness, in, inSz), 0); + AssertIntEQ(anyReqCbWantReply, 0); + + FreeChannelOpenHarness(&harness); +} + +#ifdef WOLFSSH_FWD +/* A tcpip-forward the generic callback grants is answered without the + * forward callback, so a forward can be set up from either. A port-0 + * request is the exception: only the forward callback can report the + * port bound, so a grant there is refused. */ +static void TestGlobalReqCallbackAnswersTcpipForward(void) +{ + ChannelOpenHarness harness; + byte tail[32]; + word32 tailSz; + byte in[128]; + word32 inSz; + + ResetAnyReqCb(WOLFSSH_REQ_ACCEPT); + fwdCbCallCount = 0; + InitChannelOpenHarness(&harness, NULL, 0); + AssertIntEQ(wolfSSH_CTX_SetGlobalReqCb(harness.ctx, + RecordingGlobalReqAnyCb), WS_SUCCESS); + AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, CountingFwdCb, NULL), + WS_SUCCESS); + + tailSz = AppendString(tail, sizeof(tail), 0, "localhost"); + tailSz = AppendUint32(tail, sizeof(tail), tailSz, 8080); + inSz = BuildGlobalRequestPacket("tcpip-forward", 1, tail, tailSz, + in, sizeof(in)); + + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_REQUEST_SUCCESS); + AssertIntEQ(anyReqCbCalls, 1); + AssertIntEQ(WSTRCMP(anyReqCbName, "tcpip-forward"), 0); + AssertIntEQ(fwdCbCallCount, 0); + + anyReqCbReturn = WOLFSSH_REQ_UNHANDLED; + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_REQUEST_SUCCESS); + AssertIntEQ(fwdCbCallCount, 1); + + anyReqCbReturn = WOLFSSH_REQ_ACCEPT; + tailSz = AppendString(tail, sizeof(tail), 0, "localhost"); + tailSz = AppendUint32(tail, sizeof(tail), tailSz, 0); + inSz = BuildGlobalRequestPacket("tcpip-forward", 1, tail, tailSz, + in, sizeof(in)); + AssertIntEQ(ReplyToPacket(&harness, in, inSz), MSGID_REQUEST_FAILURE); + AssertIntEQ(fwdCbCallCount, 1); + + FreeChannelOpenHarness(&harness); +} +#endif /* WOLFSSH_FWD */ + /* A username change after the first userauth request must end the session. */ static void TestUsernameChangeDisconnects(void) { @@ -4437,6 +5059,144 @@ static void TestAgentChannelNullAgentSendsOpenFail(void) FreeChannelOpenHarness(&harness); } + +/* Nothing asked for forwarding, so the open is refused rather than started. + * The refusal is the documented answer to a poll, so it must not land in + * ssh->error: wolfSSH_accept() would then abort with WS_INVALID_STATE_E. */ +static void TestAgentChannelOpenWithoutRequest(void) +{ + ChannelOpenHarness harness; + + InitChannelOpenHarness(&harness, NULL, 0); + + AssertIntEQ(wolfSSH_AGENT_ChannelOpen(harness.ssh), WS_BAD_ARGUMENT); + AssertNull(harness.ssh->agent); + AssertIntEQ(harness.io.outSz, 0); + AssertIntEQ(harness.ssh->error, WS_SUCCESS); + + /* The handshake survives the poll: no input, so accept only wants read. */ + AssertIntEQ(wolfSSH_accept(harness.ssh), WS_FATAL_ERROR); + AssertIntEQ(harness.ssh->error, WS_WANT_READ); + + FreeChannelOpenHarness(&harness); +} + +/* A poll after the peer disconnects must not open a channel or put anything + * on the wire. RFC 4253 section 11.1: the session is over. */ +static void TestAgentChannelOpenAfterDisconnect(void) +{ + ChannelOpenHarness harness; + + InitChannelOpenHarness(&harness, NULL, 0); + harness.ssh->useAgent = 1; + harness.ssh->disconnected = 1; + + AssertIntEQ(wolfSSH_AGENT_ChannelOpen(harness.ssh), WS_FATAL_ERROR); + AssertNull(harness.ssh->agent); + AssertIntEQ(harness.ssh->channelListSz, 0); + AssertIntEQ(harness.io.outSz, 0); + AssertIntEQ(harness.ssh->error, WS_DISCONNECT); + + FreeChannelOpenHarness(&harness); +} + +/* An open queued before the disconnect is not flushed either: those bytes + * belong to a session that is over, the same rule wolfSSH_shutdown() applies + * to everything but its own queued disconnect. */ +static void TestAgentChannelOpenQueuedThenDisconnect(void) +{ + ChannelOpenHarness harness; + + InitChannelOpenHarness(&harness, NULL, 0); + harness.ssh->useAgent = 1; + harness.io.blockNext = 1; + + AssertIntEQ(wolfSSH_AGENT_ChannelOpen(harness.ssh), WS_WANT_WRITE); + AssertIntEQ(harness.io.outSz, 0); + + harness.ssh->disconnected = 1; + + AssertIntEQ(wolfSSH_AGENT_ChannelOpen(harness.ssh), WS_FATAL_ERROR); + AssertIntEQ(harness.io.outSz, 0); + AssertIntEQ(harness.ssh->error, WS_DISCONNECT); + + FreeChannelOpenHarness(&harness); +} + +/* A queued open publishes the agent, so the caller's next poll must finish + * the send rather than report a success the peer never saw, and must not + * open a second channel. */ +static void TestAgentChannelOpenFlushesQueuedOpen(void) +{ + ChannelOpenHarness harness; + word32 outSz; + + InitChannelOpenHarness(&harness, NULL, 0); + harness.ssh->useAgent = 1; + harness.io.blockNext = 1; + + AssertIntEQ(wolfSSH_AGENT_ChannelOpen(harness.ssh), WS_WANT_WRITE); + AssertNotNull(harness.ssh->agent); + AssertIntEQ(harness.ssh->channelListSz, 1); + AssertIntEQ(harness.io.outSz, 0); + + AssertIntEQ(wolfSSH_AGENT_ChannelOpen(harness.ssh), WS_SUCCESS); + AssertIntEQ(harness.ssh->channelListSz, 1); + AssertTrue(harness.io.outSz > 0); + AssertIntEQ(ParseMsgId(harness.io.out, harness.io.outSz), + MSGID_CHANNEL_OPEN); + + /* The flushed open is the answer wolfSSH_accept() retries on: success, + * no second channel, no new packet, ssh->error untouched. */ + outSz = harness.io.outSz; + harness.ssh->error = WS_SUCCESS; + + AssertIntEQ(wolfSSH_AGENT_ChannelOpen(harness.ssh), WS_SUCCESS); + AssertIntEQ(harness.ssh->channelListSz, 1); + AssertIntEQ(harness.io.outSz, outSz); + AssertIntEQ(harness.ssh->error, WS_SUCCESS); + + FreeChannelOpenHarness(&harness); +} + +/* A send that fails outright, rather than blocking, leaves nothing behind, + * so a later poll starts the open over. */ +static void TestAgentChannelOpenSendFailureCleansUp(void) +{ + ChannelOpenHarness harness; + + InitChannelOpenHarness(&harness, NULL, 0); + harness.ssh->useAgent = 1; + /* No room, so MemSend reports a general error. */ + harness.io.outCap = 0; + + AssertIntEQ(wolfSSH_AGENT_ChannelOpen(harness.ssh), WS_SOCKET_ERROR_E); + AssertNull(harness.ssh->agent); + AssertIntEQ(harness.ssh->channelListSz, 0); + AssertIntEQ(harness.io.outSz, 0); + AssertIntEQ(harness.ssh->error, WS_SOCKET_ERROR_E); + + FreeChannelOpenHarness(&harness); +} + +#ifndef NO_WOLFSSH_CLIENT +/* Server-side call. A client has an ssh->agent of its own, so answering the + * poll from it would report a channel that was never opened. */ +static void TestAgentChannelOpenOnClientRefused(void) +{ + ChannelOpenHarness harness; + + InitChannelOpenHarnessClient(&harness, NULL, 0); + harness.ssh->useAgent = 1; + + AssertIntEQ(wolfSSH_AGENT_ChannelOpen(harness.ssh), WS_BAD_ARGUMENT); + AssertIntEQ(harness.ssh->channelListSz, 0); + AssertIntEQ(harness.io.outSz, 0); + AssertIntEQ(harness.ssh->error, WS_SUCCESS); + + FreeChannelOpenHarness(&harness); +} +#endif /* !NO_WOLFSSH_CLIENT */ #endif @@ -13379,6 +14139,14 @@ int main(int argc, char** argv) TestServerServiceRequestRejectedDuringKeying(); TestFailedSendClearsPendingPlaintext(); TestChannelOpenCallbackRejectSendsOpenFail(); + TestChannelReqCallbackSeesRequestAndRefuses(); + TestChannelReqCallbackGrantsUnknownType(); + TestChannelReqCallbackSettlesSessionRequest(); + TestChannelReqCallbackGrantOverridesAppChannels(); + TestGlobalReqCallbackSettlesRequest(); +#ifdef WOLFSSH_FWD + TestGlobalReqCallbackAnswersTcpipForward(); +#endif TestChannelOpenConfCallbackRuns(); TestChannelOpenFailCallbackRuns(); TestChannelOpenConfCallbackRejects(); @@ -13427,6 +14195,14 @@ int main(int argc, char** argv) #endif #ifdef WOLFSSH_AGENT TestAgentChannelNullAgentSendsOpenFail(); + TestAgentChannelOpenWithoutRequest(); + TestAgentChannelOpenFlushesQueuedOpen(); + TestAgentChannelOpenAfterDisconnect(); + TestAgentChannelOpenQueuedThenDisconnect(); + TestAgentChannelOpenSendFailureCleansUp(); +#ifndef NO_WOLFSSH_CLIENT + TestAgentChannelOpenOnClientRefused(); +#endif #endif #endif /* NO_WOLFSSH_SERVER */ #if defined(WOLFSSH_AGENT) && !defined(WOLFSSH_NO_ED25519) \ @@ -13583,6 +14359,11 @@ int main(int argc, char** argv) #ifdef KEXDH_REPLY_REGRESS_KEX_ALGO #ifndef WOLFSSH_NO_RSA_SHA2_256 + TestAppChannelsCtxInherits(); + TestAppChannelsAcceptStopsAtUserAuth(); + TestAppChannelsNoShellCbRejects(); + TestAppChannelsLateEnableReturns(); + TestSessionReqRejectedKeepsAcceptWaiting(); TestKexDhReplyRejectsRsaSha2_256SigNameDowngrade(); #endif #ifndef WOLFSSH_NO_RSA_SHA2_512 diff --git a/tests/unit.c b/tests/unit.c index 1007afcae..5e905b54d 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -8584,6 +8584,23 @@ static int CaptureMsgId(const byte* buf, word32 len) * A custom IoSend callback captures the outgoing packet in plaintext * (no cipher negotiated on a fresh session). Message ID is read via * CaptureMsgId() using LENGTH_SZ + PAD_LENGTH_SZ. */ +/* A session request callback that refuses everything, and counts. The + * callback sees the session type and command of the request it is vetting; + * what it does not see is a session already committed to the channel. */ +static int s_rejectChanReqCalls; + +static int RejectChanReqCb(WOLFSSH_CHANNEL* channel, void* ctx) +{ + (void)ctx; + s_rejectChanReqCalls++; + if (channel == NULL + || wolfSSH_ChannelGetSessionType(channel) + == WOLFSSH_SESSION_UNKNOWN) { + return 0; + } + return 1; +} + static byte s_chanReqCapture[256]; static word32 s_chanReqCaptureSz = 0; @@ -8836,6 +8853,15 @@ static int test_DoChannelRequest(void) 0x00,0x00,0x00,0x02, /* cmdSz = 2 */ 0x6C,0x73 /* "ls" */ }; + 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" */ + }; static const byte payUnknown[] = { 0x00,0x00,0x00,0x00, /* channelId = 0 */ 0x00,0x00,0x00,0x0C, /* typeSz = 12 */ @@ -8962,6 +8988,78 @@ static int test_DoChannelRequest(void) } } + /* A callback that refuses a shell, exec or subsystem request must leave + * nothing behind: no session type or command on the channel, and the + * client state short of CLIENT_DONE, or wolfSSH_accept() would go on to + * serve the session it just refused. */ + { + struct { + const char* label; + const byte* payload; + word32 payloadSz; + int errBase; + } rejCases[] = { + { "shell", payShell, (word32)sizeof(payShell), -520 }, + { "exec", payExec, (word32)sizeof(payExec), -525 }, + { "subsystem", paySubsys, (word32)sizeof(paySubsys), -530 } + }; + int r; + + wolfSSH_CTX_SetChannelReqShellCb(ctx, RejectChanReqCb); + wolfSSH_CTX_SetChannelReqExecCb(ctx, RejectChanReqCb); + wolfSSH_CTX_SetChannelReqSubsysCb(ctx, RejectChanReqCb); + + for (r = 0; r < (int)(sizeof(rejCases) / sizeof(rejCases[0])); r++) { + word32 idxRej = 0; + int retRej, capMsgId; + + s_chanReqCaptureSz = 0; + WMEMSET(s_chanReqCapture, 0, sizeof(s_chanReqCapture)); + s_rejectChanReqCalls = 0; + + retRej = wolfSSH_TestDoChannelRequest(ssh, + (byte*)rejCases[r].payload, rejCases[r].payloadSz, + &idxRej); + if (retRej != WS_SUCCESS) { + printf("DoChannelRequest[rej-%s]: ret=%d, expected=%d\n", + rejCases[r].label, retRej, WS_SUCCESS); + result = rejCases[r].errBase; + goto done; + } + if (s_rejectChanReqCalls != 1) { + printf("DoChannelRequest[rej-%s]: callback ran %d times\n", + rejCases[r].label, s_rejectChanReqCalls); + result = rejCases[r].errBase - 1; + goto done; + } + capMsgId = CaptureMsgId(s_chanReqCapture, s_chanReqCaptureSz); + if (capMsgId != (int)MSGID_CHANNEL_FAILURE) { + printf("DoChannelRequest[rej-%s]: msg_id=0x%02x, " + "expected=0x%02x\n", rejCases[r].label, capMsgId, + MSGID_CHANNEL_FAILURE); + result = rejCases[r].errBase - 2; + goto done; + } + if (ch->sessionType != WOLFSSH_SESSION_UNKNOWN + || ch->command != NULL) { + printf("DoChannelRequest[rej-%s]: session committed\n", + rejCases[r].label); + result = rejCases[r].errBase - 3; + goto done; + } + if (ssh->clientState == CLIENT_DONE) { + printf("DoChannelRequest[rej-%s]: client state changed\n", + rejCases[r].label); + result = rejCases[r].errBase - 4; + goto done; + } + } + + wolfSSH_CTX_SetChannelReqShellCb(ctx, NULL); + wolfSSH_CTX_SetChannelReqExecCb(ctx, NULL); + wolfSSH_CTX_SetChannelReqSubsysCb(ctx, NULL); + } + for (i = 0; i < (int)(sizeof(cases) / sizeof(cases[0])); i++) { word32 idx = 0; int ret; @@ -9245,6 +9343,59 @@ 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. */ + { + 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..f2bad7fb2 100644 --- a/wolfssh/agent.h +++ b/wolfssh/agent.h @@ -181,6 +181,19 @@ 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 to the client once + * the peer's auth-agent-req@openssh.com asks for forwarding. wolfSSH_accept() + * does it on the default path; an application driving its own channels polls + * this instead. Opens one channel, then flushes what of the open is queued. + * Returns WS_SUCCESS, WS_BAD_ARGUMENT before the peer asks or on a client + * session, WS_WANT_READ or WS_WANT_WRITE while output is still queued, + * WS_FATAL_ERROR with WS_DISCONNECT in ssh->error once the session is over, + * WS_SSH_NULL_E, WS_MEMORY_E, or whatever the send reports. WS_SUCCESS says + * the open went out, not that the peer took it; a refusal reaches the + * channel-open-fail callback. + * Only that and the send record in ssh->error, so a poll ahead of the peer's + * request leaves the session fit for wolfSSH_accept(). */ +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 a8001c5a6..75f2044c7 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -821,6 +821,7 @@ struct WOLFSSH_CTX { WS_CallbackUserAuthResult userAuthResultCb; /* User Authentication Result */ WS_CallbackHighwater highwaterCb; /* Data Highwater Mark Callback */ WS_CallbackGlobalReq globalReqCb; /* Global Request Callback */ + WS_CallbackGlobalReqAny globalReqAnyCb; /* Global Request, any name */ WS_CallbackReqSuccess reqSuccessCb; /* Global Request Success Callback */ WS_CallbackReqSuccess reqFailureCb; /* Global Request Failure Callback */ WS_CallbackChannelOpen channelOpenCb; /* Channel Open Requested */ @@ -829,6 +830,7 @@ struct WOLFSSH_CTX { WS_CallbackChannelReq channelReqShellCb; /* Channel Request "Shell" */ WS_CallbackChannelReq channelReqExecCb; /* Channel Request "Exec" */ WS_CallbackChannelReq channelReqSubsysCb; /* Channel Request "Subsystem" */ + WS_CallbackChannelReqAny channelReqAnyCb; /* Channel Request, any */ WS_CallbackChannelEof channelEofCb; /* Channel Eof Callback */ WS_CallbackChannelClose channelCloseCb; /* Channel Close Callback */ #ifdef WOLFSSH_SCP @@ -867,6 +869,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 +1139,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 */ @@ -1643,6 +1647,12 @@ enum ChannelOpenFailReasons { OPEN_RESOURCE_SHORTAGE }; +/* A disconnect, sent or received, ends the session, so nothing further may + * go out. RFC 4253 section 11.1. Returns 1 and records WS_DISCONNECT in + * ssh->error when the session is over, 0 otherwise. Reads are deliberately + * not gated on this: channel data that arrived before the disconnect is + * still the caller's. Call only after ssh has been checked for NULL. */ +WOLFSSH_LOCAL int SendAfterDisconnect(WOLFSSH* ssh); WOLFSSH_LOCAL int DoReceive(WOLFSSH* ssh); WOLFSSH_LOCAL int DoProtoId(WOLFSSH* ssh); WOLFSSH_LOCAL int wolfSSH_SendPacket(WOLFSSH* ssh); diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 631669b74..bd4c48c8d 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -452,6 +452,58 @@ 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); +/* What a request callback decides. UNHANDLED is what a missing callback + * answers, and leaves the request to the built-in handling. */ +typedef enum WS_ReqCbResult { + WOLFSSH_REQ_UNHANDLED = 0, + WOLFSSH_REQ_ACCEPT, + WOLFSSH_REQ_REJECT +} WS_ReqCbResult; + +/* Consulted first for every channel request, ahead of the three callbacks + * above and of the built-in handling, so a request with no callback of its + * own -- env, pty-req, window-change, exit-status, auth-agent-req, or a + * type the library does not know -- can be granted or refused by policy. + * type is the request name, NUL terminated, and data is the request's + * type-specific part, dataSz bytes, for the callback to parse. + * + * ACCEPT and REJECT settle the request, and the shell, exec and subsystem + * callbacks are not consulted. The library still parses and records what + * it needs from a request it knows, so a session request accepted here + * sets the channel's session type and the modes of an accepted pty-req are + * kept; a request that does not fit its type is refused whatever the + * callback said. A type the library does not know is answered + * CHANNEL_SUCCESS on ACCEPT, where it is otherwise refused. Shares the + * channel request context. */ +typedef int (*WS_CallbackChannelReqAny)(WOLFSSH_CHANNEL* channel, + const char* type, const byte* data, word32 dataSz, void* ctx); +WOLFSSH_API int wolfSSH_CTX_SetChannelReqCb(WOLFSSH_CTX* ctx, + WS_CallbackChannelReqAny cb); + +/* 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 later still applies to the + * channel requests that follow, but it cannot move where accept() returns + * on a session that has already gone past the user-auth stop. + * + * 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); @@ -497,6 +549,22 @@ WOLFSSH_API void wolfSSH_SetGlobalReq(WOLFSSH_CTX* ctx, WS_CallbackGlobalReq cb); WOLFSSH_API void wolfSSH_SetGlobalReqCtx(WOLFSSH* ssh, void* ctx); WOLFSSH_API void *wolfSSH_GetGlobalReqCtx(WOLFSSH* ssh); +/* Consulted first for every global request, ahead of the forward callback + * that answers tcpip-forward and cancel-tcpip-forward and of the callback + * above that answers the rest. name is the request name, NUL terminated, + * and data is the request's type-specific part, dataSz bytes, for the + * callback to parse, so a tcpip-forward can be set up from here without a + * forward callback. UNHANDLED leaves the request to those callbacks. + * ACCEPT and REJECT settle it, and no other callback is consulted; the + * reply, when one is wanted, is REQUEST_SUCCESS or REQUEST_FAILURE. A + * port-0 tcpip-forward has to be answered with the port bound, which only + * the forward callback can report, so ACCEPT on one is answered + * REQUEST_FAILURE. A client answers a tcpip-forward with failure before + * this runs, per RFC 4254 7.1. Shares the global request context. */ +typedef int (*WS_CallbackGlobalReqAny)(WOLFSSH* ssh, const char* name, + const byte* data, word32 dataSz, int wantReply, void* ctx); +WOLFSSH_API int wolfSSH_CTX_SetGlobalReqCb(WOLFSSH_CTX* ctx, + WS_CallbackGlobalReqAny cb); typedef int (*WS_CallbackReqSuccess)(WOLFSSH* ssh, void* buf, word32 sz, void* ctx); WOLFSSH_API void wolfSSH_SetReqSuccess(WOLFSSH_CTX* ctx,