diff --git a/.github/workflows/espressif.yml b/.github/workflows/espressif.yml new file mode 100644 index 000000000..72ef6958e --- /dev/null +++ b/.github/workflows/espressif.yml @@ -0,0 +1,97 @@ +name: wolfSSH Espressif Build Test + +on: + schedule: + # Weekly: Mondays at 07:00 UTC + - cron: '0 7 * * 1' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + WOLFSSL_REF: v5.9.1-stable + +jobs: + build_examples: + name: ESP-IDF ${{ matrix.idf-ref }} + strategy: + fail-fast: false + matrix: + # release-v5.5 is the current stable line; release-v5.1 is the version + # the example READMEs and VisualGDB projects document. + idf-ref: [ release-v5.5, release-v5.1 ] + runs-on: ubuntu-latest + container: + image: espressif/idf:${{ matrix.idf-ref }} + timeout-minutes: 30 + steps: + - name: Checkout wolfSSH + uses: actions/checkout@v6 + with: + path: wolfssh + + - name: Checkout wolfSSL + uses: actions/checkout@v6 + with: + repository: wolfssl/wolfssl + ref: ${{ env.WOLFSSL_REF }} + path: wolfssl + + - name: Build the Espressif examples + env: + WOLFSSH_ROOT: ${{ github.workspace }}/wolfssh + WOLFSSL_ROOT: ${{ github.workspace }}/wolfssl + run: | + # The image sets IDF_PATH, but the ESP-IDF tool paths come from + # export.sh, which a job step does not get from the entrypoint. + . "${IDF_PATH}/export.sh" + "${WOLFSSH_ROOT}/ide/Espressif/ESP-IDF/compileAllExamples.sh" + + notify_failure: + name: Open issue on scheduled failure + needs: [build_examples] + if: failure() && github.event_name == 'schedule' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + issues: write + steps: + - uses: actions/github-script@v7 + with: + script: | + const label = 'espressif-build-failure'; + const runUrl = `${context.serverUrl}/${context.repo.owner}/` + + `${context.repo.repo}/actions/runs/${context.runId}`; + const body = [ + 'The weekly Espressif example build failed.', + '', + `Run: ${runUrl}`, + `Commit: ${context.sha}`, + ].join('\n'); + const existing = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: label, + }); + if (existing.data.length > 0) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.data[0].number, + body: body, + }); + } else { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: 'Weekly Espressif example build failed', + body: body, + labels: [label], + }); + } diff --git a/ide/Espressif/ESP-IDF/compileAllExamples.sh b/ide/Espressif/ESP-IDF/compileAllExamples.sh new file mode 100755 index 000000000..f79079dfa --- /dev/null +++ b/ide/Espressif/ESP-IDF/compileAllExamples.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +# Build every wolfSSH ESP-IDF example project under ./examples/ +# +# The examples carry local wolfssh and wolfssl components that compile the +# library sources in place, so both source trees have to be on disk. Their +# CMakeLists find them through the WOLFSSH_ROOT and WOLFSSL_ROOT environment +# variables, which this script fills in when they are not already set. +# +# Run from an ESP-IDF environment: +# +# . /opt/esp/idf/export.sh +# WOLFSSL_ROOT=/path/to/wolfssl ide/Espressif/ESP-IDF/compileAllExamples.sh +# +# WOLFSSH_ROOT defaults to the tree holding this script, WOLFSSL_ROOT to a +# wolfssl checkout beside it. IDF_TARGET selects the chip; default esp32. + +if [ -z "${IDF_PATH}" ]; then + echo "ERROR: IDF_PATH is not set. Source the ESP-IDF export.sh first." + exit 1 +fi + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) + +if [ -z "${WOLFSSH_ROOT}" ]; then + WOLFSSH_ROOT=$(cd "${SCRIPT_DIR}/../../.." && pwd) +fi + +if [ -z "${WOLFSSL_ROOT}" ]; then + WOLFSSL_ROOT="$(dirname "${WOLFSSH_ROOT}")/wolfssl" +fi + +export WOLFSSH_ROOT +export WOLFSSL_ROOT +export IDF_TARGET="${IDF_TARGET:-esp32}" + +if [ ! -f "${WOLFSSH_ROOT}/wolfssh/ssh.h" ]; then + echo "ERROR: no wolfSSH source in WOLFSSH_ROOT=${WOLFSSH_ROOT}" + exit 1 +fi + +if [ ! -f "${WOLFSSL_ROOT}/wolfssl/ssl.h" ]; then + echo "ERROR: no wolfSSL source in WOLFSSL_ROOT=${WOLFSSL_ROOT}" + echo "Clone wolfssl beside wolfssh or set WOLFSSL_ROOT." + exit 1 +fi + +echo "IDF_PATH = ${IDF_PATH}" +echo "IDF_TARGET = ${IDF_TARGET}" +echo "WOLFSSH_ROOT = ${WOLFSSH_ROOT}" +echo "WOLFSSL_ROOT = ${WOLFSSL_ROOT}" + +BUILT="" +FAILED="" + +for PROJECT in "${SCRIPT_DIR}"/examples/*/; do + # A project directory is one with a top level CMakeLists.txt. + if [ ! -f "${PROJECT}/CMakeLists.txt" ]; then + continue + fi + + NAME=$(basename "${PROJECT}") + + echo + echo "--------------------------------------------------------------" + echo "Building ${NAME} for ${IDF_TARGET}" + echo "--------------------------------------------------------------" + + if (cd "${PROJECT}" && idf.py fullclean && idf.py build); then + BUILT="${BUILT} ${NAME}" + else + FAILED="${FAILED} ${NAME}" + fi +done + +if [ -z "${BUILT}" ] && [ -z "${FAILED}" ]; then + echo "ERROR: no example projects found in ${SCRIPT_DIR}/examples/" + exit 1 +fi + +echo +echo "Built: ${BUILT:- none}" + +if [ -n "${FAILED}" ]; then + echo "Failed:${FAILED}" + exit 1 +fi + +echo "All wolfSSH Espressif examples built." diff --git a/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c b/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c index 3f6f708ac..bf2912cc1 100644 --- a/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c +++ b/ide/Espressif/ESP-IDF/examples/wolfssh_echoserver/main/echoserver.c @@ -510,16 +510,23 @@ static int wolfSSH_FwdDefaultActions(WS_FwdCbAction action, void* vCtx, ctx->state = FWD_STATE_DIRECT; } else if (action == WOLFSSH_FWD_LOCAL_CLEANUP) { - WCLOSESOCKET(ctx->appFd); - if (ctx->hostName) { - WFREE(ctx->hostName, NULL, 0); - ctx->hostName = NULL; - } - if (ctx->originName) { - WFREE(ctx->originName, NULL, 0); - ctx->originName = NULL; + /* Channel id rides in port. Only its holder may tear the slot down. */ + if (port == ctx->channelId) { + /* The open can fail after setup, before anything connected. */ + if (ctx->appFd != (WS_SOCKET_T)-1) { + WCLOSESOCKET(ctx->appFd); + ctx->appFd = -1; + } + if (ctx->hostName) { + WFREE(ctx->hostName, NULL, 0); + ctx->hostName = NULL; + } + if (ctx->originName) { + WFREE(ctx->originName, NULL, 0); + ctx->originName = NULL; + } + ctx->state = FWD_STATE_INIT; } - ctx->state = FWD_STATE_INIT; } else if (action == WOLFSSH_FWD_REMOTE_SETUP) { struct sockaddr_in addr; @@ -1150,20 +1157,37 @@ static int ssh_worker(thread_ctx_t* threadCtx) } else if (rc == WS_CHANNEL_CLOSED) { #ifdef WOLFSSH_FWD - if (threadCtx->fwdCbCtx.state == FWD_STATE_CONNECTED && - lastChannel == threadCtx->fwdCbCtx.channelId) { - /* Read zero-returned. Socket is closed. Go back - to listening. */ - if (fwdFd != -1) { - WCLOSESOCKET(fwdFd); + /* wolfSSH_worker() names the channel only for the + * data and EOF statuses; DoChannelClose() recorded + * the id it retired. */ + wolfSSH_GetLastRxId(ssh, &lastChannel); + if (lastChannel == threadCtx->fwdCbCtx.channelId) { + /* Held bytes belong to the channel going away. */ + fwdBufferIdx = 0; + if (threadCtx->fwdCbCtx.appFd == (WS_SOCKET_T)-1) { + /* The cleanup handler ran ahead of this and + * closed the socket; only this copy of the + * descriptor is stale. */ fwdFd = -1; } - if (threadCtx->fwdCbCtx.originName != NULL) { - WFREE(threadCtx->fwdCbCtx.originName, - NULL, 0); - threadCtx->fwdCbCtx.originName = NULL; + else if (threadCtx->fwdCbCtx.state + == FWD_STATE_CONNECTED) { + /* A locally opened forward is armed by no + * LOCAL_SETUP and so draws no cleanup. Its + * teardown is still ours: go back to + * listening. */ + if (fwdFd != -1) { + WCLOSESOCKET(fwdFd); + fwdFd = -1; + threadCtx->fwdCbCtx.appFd = -1; + } + if (threadCtx->fwdCbCtx.originName != NULL) { + WFREE(threadCtx->fwdCbCtx.originName, + NULL, 0); + threadCtx->fwdCbCtx.originName = NULL; + } + threadCtx->fwdCbCtx.state = FWD_STATE_LISTEN; } - threadCtx->fwdCbCtx.state = FWD_STATE_LISTEN; } #endif continue; @@ -1290,6 +1314,8 @@ static int ssh_worker(thread_ctx_t* threadCtx) to listening. */ WCLOSESOCKET(fwdFd); fwdFd = -1; + fwdBufferIdx = 0; + threadCtx->fwdCbCtx.appFd = -1; if (threadCtx->fwdCbCtx.hostName != NULL) { WFREE(threadCtx->fwdCbCtx.hostName, NULL, 0); @@ -1310,6 +1336,9 @@ static int ssh_worker(thread_ctx_t* threadCtx) /* Connection reset. Socket is closed. * Go back to listening. */ WCLOSESOCKET(fwdFd); + fwdFd = -1; + fwdBufferIdx = 0; + threadCtx->fwdCbCtx.appFd = -1; threadCtx->fwdCbCtx.state = FWD_STATE_LISTEN; continue; } @@ -1405,6 +1434,9 @@ static int ssh_worker(thread_ctx_t* threadCtx) threadCtx->fwdCbCtx.hostPort); if (fwdFd > 0) { + /* The cleanup handler closes what it finds here, so a + * direct forward has to record its socket too. */ + threadCtx->fwdCbCtx.appFd = fwdFd; threadCtx->fwdCbCtx.state = FWD_STATE_CONNECTED; } }