diff --git a/.github/workflows/check-go-dependencies-task.yml b/.github/workflows/check-go-dependencies-task.yml index 4983e0db6..417c525e5 100644 --- a/.github/workflows/check-go-dependencies-task.yml +++ b/.github/workflows/check-go-dependencies-task.yml @@ -3,7 +3,7 @@ name: Check Go Dependencies env: # See: https://github.com/actions/setup-go/tree/v3#readme - GO_VERSION: "1.21" + GO_VERSION: "1.23" # See: https://docs.github.com/actions/using-workflows/events-that-trigger-workflows on: @@ -72,11 +72,17 @@ jobs: with: submodules: recursive + # This is required to allow licensee/setup-licensed to install Licensed via Ruby gem. + - name: Install Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ruby # Install latest version + - name: Install licensed - uses: jonabc/setup-licensed@v1 + uses: licensee/setup-licensed@v1.3.2 with: github_token: ${{ secrets.GITHUB_TOKEN }} - version: 3.x + version: 5.x - name: Install Go uses: actions/setup-go@v5 @@ -108,6 +114,7 @@ jobs: uses: actions/upload-artifact@v4 with: if-no-files-found: error + include-hidden-files: true name: dep-licenses-cache path: ${{ env.CACHE_PATH }} @@ -122,11 +129,17 @@ jobs: with: submodules: recursive + # This is required to allow licensee/setup-licensed to install Licensed via Ruby gem. + - name: Install Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ruby # Install latest version + - name: Install licensed - uses: jonabc/setup-licensed@v1 + uses: licensee/setup-licensed@v1.3.2 with: github_token: ${{ secrets.GITHUB_TOKEN }} - version: 3.x + version: 5.x - name: Install Go uses: actions/setup-go@v5 diff --git a/.github/workflows/check-go-task.yml b/.github/workflows/check-go-task.yml index 7ea76eb48..21478d23e 100644 --- a/.github/workflows/check-go-task.yml +++ b/.github/workflows/check-go-task.yml @@ -3,7 +3,7 @@ name: Check Go env: # See: https://github.com/actions/setup-go/tree/main#supported-version-syntax - GO_VERSION: "1.21" + GO_VERSION: "1.23" # See: https://docs.github.com/actions/using-workflows/events-that-trigger-workflows on: @@ -82,7 +82,7 @@ jobs: version: 3.x - name: Install Dependencies - run: sudo apt update && sudo apt install -y --no-install-recommends build-essential libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev + run: sudo apt update && sudo apt install -y --no-install-recommends build-essential libgtk-3-dev libwebkit2gtk-4.1-0 libappindicator3-dev - name: Check for errors env: diff --git a/.github/workflows/publish-go-tester-task.yml b/.github/workflows/publish-go-tester-task.yml index 66b381643..89d5c2b45 100644 --- a/.github/workflows/publish-go-tester-task.yml +++ b/.github/workflows/publish-go-tester-task.yml @@ -30,7 +30,8 @@ on: repository_dispatch: env: - GO_VERSION: "1.21" + PROJECT_NAME: arduino-cloud-agent + GO_VERSION: "1.23" jobs: run-determination: @@ -64,7 +65,7 @@ jobs: #use the strategy instead because we still use the native build strategy: matrix: - os: [ubuntu-20.04, windows-2019, macos-12] + os: [ubuntu-22.04, windows-2019, macos-13] arch: [-amd64] include: - os: windows-2019 @@ -119,18 +120,38 @@ jobs: run: task go:build-win # GOARCH=amd64 by default on the runners if: runner.os == 'Windows' && matrix.arch == '-amd64' - - name: Build the Agent for macos + - name: Build the Agent for macos amd 64 env: MACOSX_DEPLOYMENT_TARGET: 10.15 # minimum supported version for mac CGO_CFLAGS: -mmacosx-version-min=10.15 CGO_LDFLAGS: -mmacosx-version-min=10.15 - run: task go:build + run: | + task go:build + mv ${{ env.PROJECT_NAME }} ${{ env.PROJECT_NAME}}_amd64 + if: runner.os == 'macOS' + + - name: Build the Agent for macos arm 64 + env: + MACOSX_DEPLOYMENT_TARGET: 10.15 # minimum supported version for mac + CGO_CFLAGS: -mmacosx-version-min=10.15 + CGO_LDFLAGS: -mmacosx-version-min=10.15 + GOARCH: arm64 + CGO_ENABLED: 1 + run: | + task go:build + mv ${{ env.PROJECT_NAME }} ${{ env.PROJECT_NAME}}_arm64 + if: runner.os == 'macOS' + + - name: Create universal macos executable + run: | + lipo -create -output ${{ env.PROJECT_NAME }} ${{ env.PROJECT_NAME}}_amd64 ${{ env.PROJECT_NAME}}_arm64 + rm ${{ env.PROJECT_NAME}}_amd64 ${{ env.PROJECT_NAME}}_arm64 if: runner.os == 'macOS' - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: arduino-create-agent-${{ matrix.os }}${{ matrix.arch }} + name: ${{ env.PROJECT_NAME}}-${{ matrix.os }}${{ matrix.arch }} path: | - arduino-create-agent* + ${{ env.PROJECT_NAME}}* if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ce3caa1b7..12b3781ba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ permissions: env: # As defined by the Taskfile's PROJECT_NAME variable - PROJECT_NAME: arduino-create-agent + PROJECT_NAME: arduino-cloud-agent TARGET: "/CreateAgent/Stable/" VERSION_TARGET: "arduino-create-static/agent-metadata/" AWS_REGION: "us-east-1" # or https://github.com/aws/aws-cli/issues/5623 @@ -23,7 +23,7 @@ env: AC_PASSWORD: ${{ secrets.AC_PASSWORD }} # used by gon AC_PROVIDER: ${{ secrets.AC_PROVIDER }} # used by gon # See: https://github.com/actions/setup-go/tree/v3#readme - GO_VERSION: "1.21" + GO_VERSION: "1.23" jobs: # The build job is responsible for: configuring the environment, testing and compiling process @@ -32,7 +32,7 @@ jobs: prerelease: ${{ steps.prerelease.outputs.IS_PRE }} strategy: matrix: - os: [ubuntu-20.04, windows-2019, macos-12] + os: [ubuntu-22.04, windows-2019, macos-13] arch: [amd64] include: - os: windows-2019 @@ -88,7 +88,7 @@ jobs: - name: Build the Agent for linux run: task go:build - if: matrix.os == 'ubuntu-20.04' + if: matrix.os == 'ubuntu-22.04' # the manifest is required by windows GUI apps, otherwise the binary will crash with: "Unable to create main window: TTM_ADDTOOL failed" (for reference https://github.com/lxn/walk/issues/28) # rsrc will produce a *.syso file that should get automatically recognized by go build command and linked into an executable. @@ -107,32 +107,51 @@ jobs: run: task go:build-win # GOARCH=amd64 by default on the runners if: matrix.os == 'windows-2019' && matrix.arch == 'amd64' - - name: Build the Agent for macos + - name: Build the Agent for macos amd64 env: CGO_ENABLED: 1 MACOSX_DEPLOYMENT_TARGET: 10.15 # minimum supported version for mac CGO_CFLAGS: -mmacosx-version-min=10.15 CGO_LDFLAGS: -mmacosx-version-min=10.15 - run: task go:build - if: matrix.os == 'macos-12' + run: | + task go:build + mv ${{ env.PROJECT_NAME }} ${{ env.PROJECT_NAME }}_amd64 + if: matrix.os == 'macos-13' + + - name: Build the Agent for macos arm64 + env: + CGO_ENABLED: 1 + MACOSX_DEPLOYMENT_TARGET: 10.15 # minimum supported version for mac + CGO_CFLAGS: -mmacosx-version-min=10.15 + CGO_LDFLAGS: -mmacosx-version-min=10.15 + GOARCH: arm64 + run: | + task go:build + mv ${{ env.PROJECT_NAME }} ${{ env.PROJECT_NAME }}_arm64 + if: matrix.os == 'macos-13' + + - name: Create universal macos executable + run: | + lipo -create -output ${{ env.PROJECT_NAME }} ${{ env.PROJECT_NAME }}_amd64 ${{ env.PROJECT_NAME }}_arm64 + rm ${{ env.PROJECT_NAME }}_amd64 ${{ env.PROJECT_NAME }}_arm64 + if: matrix.os == 'macos-13' # this will create `public/` dir with compressed full bin (/-.gz) and a json file - name: Create autoupdate files run: go-selfupdate ${{ env.PROJECT_NAME }}${{ matrix.ext }} ${TAG_VERSION} if: matrix.arch != '386' && steps.prerelease.outputs.IS_PRE != 'true' - # for now we do not distribute m1 build, this is a workaround for now - name: Copy autoupdate file for darwin-arm64 (m1 arch) working-directory: public/ run: | cp darwin-amd64.json darwin-arm64.json cp ${TAG_VERSION}/darwin-amd64.gz ${TAG_VERSION}/darwin-arm64.gz - if: matrix.os == 'macos-12' && steps.prerelease.outputs.IS_PRE != 'true' + if: matrix.os == 'macos-13' && steps.prerelease.outputs.IS_PRE != 'true' - name: Create autoupdate files for win32 run: go-selfupdate -platform windows-${{ matrix.arch }} ${{ env.PROJECT_NAME }}${{ matrix.ext }} ${TAG_VERSION} if: matrix.arch == '386' && matrix.os == 'windows-2019' && steps.prerelease.outputs.IS_PRE != 'true' - + - name: configure aws credentials uses: aws-actions/configure-aws-credentials@v4 with: @@ -162,9 +181,9 @@ jobs: matrix: arch: [amd64, arm64] - runs-on: macos-12 + runs-on: macos-13 env: - EXE_PATH: "skel/ArduinoCreateAgent.app/Contents/MacOS/" + EXE_PATH: "skel/ArduinoCloudAgent.app/Contents/MacOS/" steps: - name: Checkout @@ -176,7 +195,7 @@ jobs: - name: Download artifact uses: actions/download-artifact@v4 with: - name: ${{ env.PROJECT_NAME }}-macos-12-amd64 # if we want to support darwin-arm64 in the future for real this has to change. + name: ${{ env.PROJECT_NAME }}-macos-13-amd64 # if we want to support darwin-arm64 in the future for real this has to change. path: ${{ env.EXE_PATH }} - name: Remove placeholder file @@ -186,21 +205,21 @@ jobs: - name: Make executable run: chmod -v +x ${{ env.EXE_PATH }}${{ env.PROJECT_NAME }} - - name: Rename executable to Arduino_Create_Agent - run: mv -v ${{ env.EXE_PATH }}${{ env.PROJECT_NAME }} ${{ env.EXE_PATH }}Arduino_Create_Agent + - name: Rename executable to Arduino_Cloud_Agent + run: mv -v ${{ env.EXE_PATH }}${{ env.PROJECT_NAME }} ${{ env.EXE_PATH }}Arduino_Cloud_Agent - name: get year run: echo "YEAR=$(date "+%Y")" >> $GITHUB_ENV - name: Generate Info.plist for MacOS run: | - cat > skel/ArduinoCreateAgent.app/Contents/Info.plist < skel/ArduinoCloudAgent.app/Contents/Info.plist <CFBundlePackageTypeAPPLCFBundleInfoDictionaryVersion6.0 CFBundleIconFile AppIcon.icns - CFBundleName Arduino Create Agent - CFBundleExecutable Arduino_Create_Agent + CFBundleName Arduino Cloud Agent + CFBundleExecutable Arduino_Cloud_Agent CFBundleIdentifier create.arduino.cc CFBundleVersion ${GITHUB_REF##*/} @@ -215,14 +234,14 @@ jobs: EOF - name: Tar bundle to keep permissions - run: tar -cvf ArduinoCreateAgent.app_${{ matrix.arch }}.tar -C skel/ . + run: tar -cvf ArduinoCloudAgent.app_${{ matrix.arch }}.tar -C skel/ . - name: Upload artifacts uses: actions/upload-artifact@v4 with: if-no-files-found: error - name: ArduinoCreateAgent.app_${{ matrix.arch }} - path: ArduinoCreateAgent.app_${{ matrix.arch }}.tar + name: ArduinoCloudAgent.app_${{ matrix.arch }} + path: ArduinoCloudAgent.app_${{ matrix.arch }}.tar # The notarize-macos job will download the macos bundle from the previous job, sign, notarize and re-upload it, uploading it also on s3 download servers for the autoupdate. notarize-macos: @@ -233,20 +252,25 @@ jobs: matrix: arch: [amd64, arm64] - runs-on: macos-12 + runs-on: macos-13 env: GON_PATH: ${{ github.workspace }}/gon needs: [build, create-macos-bundle] environment: production steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + - name: Download artifact uses: actions/download-artifact@v4 with: - name: ArduinoCreateAgent.app_${{ matrix.arch }} + name: ArduinoCloudAgent.app_${{ matrix.arch }} - name: un-Tar bundle - run: tar -xvf ArduinoCreateAgent.app_${{ matrix.arch }}.tar + run: tar -xvf ArduinoCloudAgent.app_${{ matrix.arch }}.tar - name: Import Code-Signing Certificates run: | @@ -286,7 +310,7 @@ jobs: run: | cat > "${{ env.GON_CONFIG_PATH }}" < "${{ env.GON_CONFIG_PATH }}" < /tmp/license.xml @@ -425,14 +450,14 @@ jobs: run: ${{ env.INSTALLBUILDER_PATH }} build installer.xml ${{ matrix.installbuilder-name }} --verbose --license /tmp/license.xml --setvars ${{ env.INSTALLER_VARS }} architecture=${{ matrix.arch }} - name: Generate archive - run: tar -czvf ArduinoCreateAgent-${GITHUB_REF##*/}-${{ matrix.platform-name }}-${{ matrix.arch }}-installer.tar.gz ArduinoCreateAgent-${GITHUB_REF##*/}-${{ matrix.platform-name }}-${{ matrix.arch }}-installer${{matrix.installer-extension}} - if: matrix.os == 'ubuntu-20.04' + run: tar -czvf ArduinoCloudAgent-${GITHUB_REF##*/}-${{ matrix.platform-name }}-${{ matrix.arch }}-installer.tar.gz ArduinoCloudAgent-${GITHUB_REF##*/}-${{ matrix.platform-name }}-${{ matrix.arch }}-installer${{matrix.installer-extension}} + if: matrix.os == 'ubuntu-22.04' - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: ArduinoCreateAgent-${{ matrix.platform-name }}-${{ matrix.arch }} - path: ArduinoCreateAgent* + name: ArduinoCloudAgent-${{ matrix.platform-name }}-${{ matrix.arch }} + path: ArduinoCloudAgent* if-no-files-found: error # This job will sign the Windows installer @@ -449,17 +474,17 @@ jobs: # We are hardcoding the path for signtool because is not present on the windows PATH env var by default. # Keep in mind that this path could change when upgrading to a new runner version SIGNTOOL_PATH: "C:/Program Files (x86)/Windows Kits/10/bin/10.0.19041.0/x86/signtool.exe" - + strategy: matrix: arch: [amd64, 386] - + steps: - name: Download artifact uses: actions/download-artifact@v4 with: - name: ArduinoCreateAgent-windows-${{ matrix.arch }} - + name: ArduinoCloudAgent-windows-${{ matrix.arch }} + - name: Save Win signing certificate to file run: echo "${{ secrets.INSTALLER_CERT_WINDOWS_CER }}" | base64 --decode > ${{ env.INSTALLER_CERT_WINDOWS_CER}} @@ -468,15 +493,19 @@ jobs: CERT_PASSWORD: ${{ secrets.INSTALLER_CERT_WINDOWS_PASSWORD }} CONTAINER_NAME: ${{ secrets.INSTALLER_CERT_WINDOWS_CONTAINER }} # https://stackoverflow.com/questions/17927895/automate-extended-validation-ev-code-signing-with-safenet-etoken - run: | - "${{ env.SIGNTOOL_PATH }}" sign -d "Arduino Create Agent" -f ${{ env.INSTALLER_CERT_WINDOWS_CER}} -csp "eToken Base Cryptographic Provider" -k "[{{${{ env.CERT_PASSWORD }}}}]=${{ env.CONTAINER_NAME }}" -fd sha256 -tr http://timestamp.digicert.com -td SHA256 -v "ArduinoCreateAgent-${GITHUB_REF##*/}-windows-${{ matrix.arch }}-installer.exe" + run: | + "${{ env.SIGNTOOL_PATH }}" sign -d "Arduino Cloud Agent" -f ${{ env.INSTALLER_CERT_WINDOWS_CER}} -csp "eToken Base Cryptographic Provider" -k "[{{${{ env.CERT_PASSWORD }}}}]=${{ env.CONTAINER_NAME }}" -fd sha256 -tr http://timestamp.digicert.com -td SHA256 -v "ArduinoCloudAgent-${GITHUB_REF##*/}-windows-${{ matrix.arch }}-installer.exe" - name: Upload artifacts uses: actions/upload-artifact@v4 with: if-no-files-found: error - name: ArduinoCreateAgent-windows-${{ matrix.arch }}-signed - path: ArduinoCreateAgent-*-windows-${{ matrix.arch }}-installer.exe + name: ArduinoCloudAgent-windows-${{ matrix.arch }}-signed + path: ArduinoCloudAgent-*-windows-${{ matrix.arch }}-installer.exe + + # This step is needed because the self hosted runner does not delete files automatically + - name: Clean up EXE + run: rm ArduinoCloudAgent-*-windows-${{ matrix.arch }}-installer.exe # This job will generate a dmg mac installer, sign/notarize it. generate-sign-dmg: @@ -485,7 +514,7 @@ jobs: matrix: arch: [amd64] - runs-on: macos-12 + runs-on: macos-13 steps: - name: Checkout repo with icons/background uses: actions/checkout@v4 @@ -496,11 +525,11 @@ jobs: - name: Download artifact uses: actions/download-artifact@v4 with: - name: ArduinoCreateAgent.app_${{ matrix.arch }}_notarized - path: ArduinoCreateAgent.app + name: ArduinoCloudAgent.app_${{ matrix.arch }}_notarized + path: ArduinoCloudAgent.app - name: unzip artifact - working-directory: ArduinoCreateAgent.app + working-directory: ArduinoCloudAgent.app run: | unzip ArduinoCreateAgent.app_${{ matrix.arch }}_notarized.zip rm ArduinoCreateAgent.app_${{ matrix.arch }}_notarized.zip @@ -508,18 +537,18 @@ jobs: - name: Install create-dmg run: brew install create-dmg - - name: Genarate DMG + - name: Generate DMG run: | create-dmg \ - --volname "ArduinoCreateAgent" \ + --volname "ArduinoCloudAgent" \ --background "installer_icons/background.tiff" \ --window-pos 200 120 \ --window-size 500 320 \ --icon-size 80 \ - --icon "ArduinoCreateAgent.app" 125 150 \ + --icon "ArduinoCloudAgent.app" 125 150 \ --app-drop-link 375 150 \ - "ArduinoCreateAgent-${GITHUB_REF##*/}-osx-${{ matrix.arch }}-installer.dmg" \ - "ArduinoCreateAgent.app" + "ArduinoCloudAgent-${GITHUB_REF##*/}-osx-${{ matrix.arch }}-installer.dmg" \ + "ArduinoCloudAgent.app" - name: Import Code-Signing Certificates run: | @@ -549,7 +578,7 @@ jobs: # gon does not allow env variables in config file (https://github.com/mitchellh/gon/issues/20) run: | cat > gon.config_installer.hcl <| +------------>| | -| | | | | Arduino Create Agent | | Arduino Board | -| | Arduino Create Web Editor | +--------------->| |<------------+ | +| | | | | Arduino Cloud Agent | | Arduino Board | +| | Arduino Cloud | +--------------->| |<------------+ | | | | | REST API +----------------------+ serial +---------------+ | +---------------------------+ | +-------------------------------+ @@ -27,12 +27,12 @@ The Arduino Create Agent is a single binary that will sit on the traybar and wor ## Installation -Get the [latest version](https://github.com/arduino/arduino-create-agent/releases) of the Agent for all supported platforms or complete the [Getting Started](https://create.arduino.cc/getting-started/plugin/welcome). +Get the [latest version](https://github.com/arduino/arduino-create-agent/releases) of the Agent for all supported platforms or complete the [Getting Started](https://cloud.arduino.cc/download-agent/). ## Apple silicon support -The Arduino Agent is supported both on Intel and Apple silicon computers. This includes devices with the M1, M2 and M3 processors. -At the moment the Arduino Agent is only built for Intel architectures, but Apple silicon devices can run it thanks to the [Rosetta 2](https://support.apple.com/en-us/HT211861) translation layer by Apple. +The Arduino Agent is supported both on Intel and Apple silicon computers. This includes devices with the M1, M2 and M3 processors. +The Arduino Agent is built both for Intel architectures and Apple silicon devices, but distributed as a single universal executable for macOS. ## Documentation @@ -40,7 +40,7 @@ The documentation has been moved to the [wiki](https://github.com/arduino/arduin - [Advanced usage](https://github.com/arduino/arduino-create-agent/wiki/Advanced-usage): explaining how to use multiple configurations and how to use the agent with a proxy. - [Agent Beta Program](https://github.com/arduino/arduino-create-agent/wiki/Agent-Beta-Program) -- [Development](https://github.com/arduino/arduino-create-agent/wiki/Developement): containing useful info to help in development +- [Development](https://github.com/arduino/arduino-create-agent/wiki/Development): containing useful info to help in development - [Disable Autostart](https://github.com/arduino/arduino-create-agent/wiki/Disable-Autostart) - [How to compile on Raspberry Pi](https://github.com/arduino/arduino-create-agent/wiki/How-to-compile-on-Raspberry-Pi) - [How to use crashreport functionality](https://github.com/arduino/arduino-create-agent/wiki/How-to-use-crashreport-functionality) @@ -50,7 +50,7 @@ The documentation has been moved to the [wiki](https://github.com/arduino/arduin ### Submitting an issue -When submitting a new issue please search for duplicates before creating a new one. Help us by providing useful context and information. Please attach the output of the commands running at the debug console or attach [crash reports](https://github.com/arduino/arduino-create-agent/wiki/How-to-use-crashreport-functionality) if useful. +When submitting a new issue please search for duplicates before creating a new one. Help us by providing useful context and information. Please attach the output of the commands running at the debug console or attach [crash reports](https://github.com/arduino/arduino-create-agent/wiki/How-to-use-crashreport-functionality) if useful. #### Security @@ -74,7 +74,7 @@ By signing off your commits, you agree to the following agreement, also known as ## Authors and acknowledgment -arduino-create-agent is a fork of @[johnlauer](https://github.com/johnlauer)'s [serial-port-json-server](https://github.com/johnlauer/serial-port-json-server) (which we really want to thank for his kindness and great work) +arduino-cloud-agent is a fork of @[johnlauer](https://github.com/johnlauer)'s [serial-port-json-server](https://github.com/johnlauer/serial-port-json-server) (which we really want to thank for his kindness and great work) The history has been rewritten to keep the repo small (thus removing all binaries committed in the past) diff --git a/Taskfile.yml b/Taskfile.yml index 6b7852762..681c8274d 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1,6 +1,17 @@ version: "3" tasks: + + install: + desc: Install dependencies for local development + cmds: + - go install github.com/githubnemo/CompileDaemon@v1.4.0 + + run: + desc: Run the project locally with auto-reload and data race detector + cmds: + - CompileDaemon -build="go build -race" -command="./arduino-create-agent" -graceful-kill=true + # Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/check-dependencies-task/Taskfile.yml general:cache-dep-licenses: desc: Cache dependency license metadata @@ -37,7 +48,7 @@ tasks: cmds: - task: go:build vars: - PROJECT_NAME: arduino-create-agent_cli + PROJECT_NAME: arduino-cloud-agent_cli ADDITIONAL_FLAGS: -tags cli go:build-win: @@ -46,7 +57,7 @@ tasks: - rsrc -arch {{.GOARCH}} -manifest manifest.xml # GOARCH shoud be either amd64 or 386 - task: go:build vars: - PROJECT_NAME: arduino-create-agent.exe + PROJECT_NAME: arduino-cloud-agent.exe WIN_FLAGS: -H=windowsgui - rm *.syso # rm file to avoid compilation problems on other platforms vars: @@ -58,7 +69,7 @@ tasks: cmds: - task: go:build vars: - PROJECT_NAME: arduino-create-agent_cli.exe + PROJECT_NAME: arduino-cloud-agent_cli.exe ADDITIONAL_FLAGS: -tags cli # Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/test-go-task/Taskfile.yml @@ -84,7 +95,7 @@ tasks: cmds: - poetry run pytest tests - # Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/poetry-task/Taskfile.yml + # Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/poetry-task/Taskfile.yml poetry:install-deps: desc: Install dependencies managed by Poetry cmds: @@ -138,10 +149,23 @@ tasks: - task: go:vet - task: go:lint - + autoupdate:demo: + desc: Demo the local auto-update workflow for the Agent (for linux and mac user) + prompt: Before continuing, please make sure you’ve opened the "Open Configuration" option from the Agent menu and set the updateUrl=http://127.0.0.1:3000/ + cmds: + - task: go:build + - go install github.com/sanbornm/go-selfupdate/...@latest + # NOTE: the path 'CreateAgent/Stable' is important to keep as is + # because agent searches the update files into "CreateAgent/Stable/{platform}.json" and "https://downloads.arduino.cc/CreateAgent/Stable/{version}/{platform}.gz" + - go-selfupdate -o public/CreateAgent/Stable ./arduino-cloud-agent {{.VERSION}} + - docker rm -f agent-static-server + - docker run --rm -d -v "$PWD/public:/usr/share/nginx/html:ro" -p 3000:80 --name agent-static-server nginx:alpine + - sleep 5 # wait the server is up before starting the update + - curl -X POST http://127.0.0.1:8991/update + vars: # Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/release-go-task/Taskfile.yml - PROJECT_NAME: arduino-create-agent + PROJECT_NAME: arduino-cloud-agent # build vars COMMIT: sh: echo "$(git log --no-show-signature -n 1 --format=%h)" diff --git a/conn.go b/conn.go index 727a5cadb..8c71c54c4 100644 --- a/conn.go +++ b/conn.go @@ -19,6 +19,7 @@ package main import ( "bytes" + "crypto/rsa" "encoding/json" "errors" "fmt" @@ -79,109 +80,114 @@ type Upload struct { var uploadStatusStr = "ProgrammerStatus" -func uploadHandler(c *gin.Context) { - - data := new(Upload) - c.BindJSON(data) - - log.Printf("%+v %+v %+v %+v %+v %+v", data.Port, data.Board, data.Rewrite, data.Commandline, data.Extra, data.Filename) - - if data.Port == "" { - c.String(http.StatusBadRequest, "port is required") - return - } - - if data.Board == "" { - c.String(http.StatusBadRequest, "board is required") - log.Error("board is required") - return - } - - if !data.Extra.Network { - if data.Signature == "" { - c.String(http.StatusBadRequest, "signature is required") +func uploadHandler(pubKey *rsa.PublicKey) func(*gin.Context) { + return func(c *gin.Context) { + data := new(Upload) + if err := c.BindJSON(data); err != nil { + c.String(http.StatusBadRequest, fmt.Sprintf("err with the payload. %v", err.Error())) return } - if data.Commandline == "" { - c.String(http.StatusBadRequest, "commandline is required for local board") + log.Printf("%+v %+v %+v %+v %+v %+v", data.Port, data.Board, data.Rewrite, data.Commandline, data.Extra, data.Filename) + + if data.Port == "" { + c.String(http.StatusBadRequest, "port is required") return } - err := utilities.VerifyInput(data.Commandline, data.Signature) - - if err != nil { - c.String(http.StatusBadRequest, "signature is invalid") + if data.Board == "" { + c.String(http.StatusBadRequest, "board is required") + log.Error("board is required") return } - } - buffer := bytes.NewBuffer(data.Hex) + if !data.Extra.Network { + if data.Signature == "" { + c.String(http.StatusBadRequest, "signature is required") + return + } - filePath, err := utilities.SaveFileonTempDir(data.Filename, buffer) - if err != nil { - c.String(http.StatusBadRequest, err.Error()) - return - } + if data.Commandline == "" { + c.String(http.StatusBadRequest, "commandline is required for local board") + return + } - tmpdir, err := os.MkdirTemp("", "extrafiles") - if err != nil { - c.String(http.StatusBadRequest, err.Error()) - return - } + err := utilities.VerifyInput(data.Commandline, data.Signature, pubKey) - for _, extraFile := range data.ExtraFiles { - path, err := utilities.SafeJoin(tmpdir, extraFile.Filename) - if err != nil { - c.String(http.StatusBadRequest, err.Error()) - return + if err != nil { + log.WithField("err", err).Error("Error verifying the command") + c.String(http.StatusBadRequest, "signature is invalid") + return + } } - log.Printf("Saving %s on %s", extraFile.Filename, path) - err = os.MkdirAll(filepath.Dir(path), 0744) - if err != nil { - c.String(http.StatusBadRequest, err.Error()) - return - } + buffer := bytes.NewBuffer(data.Hex) - err = os.WriteFile(path, extraFile.Hex, 0644) + filePath, err := utilities.SaveFileonTempDir(data.Filename, buffer) if err != nil { c.String(http.StatusBadRequest, err.Error()) return } - } - if data.Rewrite != "" { - data.Board = data.Rewrite - } - - go func() { - // Resolve commandline - commandline, err := upload.PartiallyResolve(data.Board, filePath, tmpdir, data.Commandline, data.Extra, Tools) + tmpdir, err := os.MkdirTemp("", "extrafiles") if err != nil { - send(map[string]string{uploadStatusStr: "Error", "Msg": err.Error()}) + c.String(http.StatusBadRequest, err.Error()) return } - l := PLogger{Verbose: true} - - // Upload - if data.Extra.Network { - err = errors.New("network upload is not supported anymore, pease use OTA instead") - } else { - send(map[string]string{uploadStatusStr: "Starting", "Cmd": "Serial"}) - err = upload.Serial(data.Port, commandline, data.Extra, l) + for _, extraFile := range data.ExtraFiles { + path, err := utilities.SafeJoin(tmpdir, extraFile.Filename) + if err != nil { + c.String(http.StatusBadRequest, err.Error()) + return + } + log.Printf("Saving %s on %s", extraFile.Filename, path) + + err = os.MkdirAll(filepath.Dir(path), 0744) + if err != nil { + c.String(http.StatusBadRequest, err.Error()) + return + } + + err = os.WriteFile(path, extraFile.Hex, 0644) + if err != nil { + c.String(http.StatusBadRequest, err.Error()) + return + } } - // Handle result - if err != nil { - send(map[string]string{uploadStatusStr: "Error", "Msg": err.Error()}) - return + if data.Rewrite != "" { + data.Board = data.Rewrite } - send(map[string]string{uploadStatusStr: "Done", "Flash": "Ok"}) - }() - c.String(http.StatusAccepted, "") + go func() { + // Resolve commandline + commandline, err := upload.PartiallyResolve(data.Board, filePath, tmpdir, data.Commandline, data.Extra, Tools) + if err != nil { + send(map[string]string{uploadStatusStr: "Error", "Msg": err.Error()}) + return + } + + l := PLogger{Verbose: true} + + // Upload + if data.Extra.Network { + err = errors.New("network upload is not supported anymore, pease use OTA instead") + } else { + send(map[string]string{uploadStatusStr: "Starting", "Cmd": "Serial"}) + err = upload.Serial(data.Port, commandline, data.Extra, l) + } + + // Handle result + if err != nil { + send(map[string]string{uploadStatusStr: "Error", "Msg": err.Error()}) + return + } + send(map[string]string{uploadStatusStr: "Done", "Flash": "Ok"}) + }() + + c.String(http.StatusAccepted, "") + } } // PLogger sends the info from the upload to the websocket diff --git a/globals/globals.go b/globals/globals.go index d7cb09a17..ac4c14666 100644 --- a/globals/globals.go +++ b/globals/globals.go @@ -15,8 +15,15 @@ package globals -// DefaultIndexURL is the default index url var ( - // SignatureKey is the public key used to verify commands and url sent by the builder - SignatureKey = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvc0yZr1yUSen7qmE3cxF\nIE12rCksDnqR+Hp7o0nGi9123eCSFcJ7CkIRC8F+8JMhgI3zNqn4cUEn47I3RKD1\nZChPUCMiJCvbLbloxfdJrUi7gcSgUXrlKQStOKF5Iz7xv1M4XOP3JtjXLGo3EnJ1\npFgdWTOyoSrA8/w1rck4c/ISXZSinVAggPxmLwVEAAln6Itj6giIZHKvA2fL2o8z\nCeK057Lu8X6u2CG8tRWSQzVoKIQw/PKK6CNXCAy8vo4EkXudRutnEYHEJlPkVgPn\n2qP06GI+I+9zKE37iqj0k1/wFaCVXHXIvn06YrmjQw6I0dDj/60Wvi500FuRVpn9\ntwIDAQAB\n-----END PUBLIC KEY-----" + // ArduinoSignaturePubKey is the public key used to verify commands and url sent by the builder + ArduinoSignaturePubKey = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvc0yZr1yUSen7qmE3cxF +IE12rCksDnqR+Hp7o0nGi9123eCSFcJ7CkIRC8F+8JMhgI3zNqn4cUEn47I3RKD1 +ZChPUCMiJCvbLbloxfdJrUi7gcSgUXrlKQStOKF5Iz7xv1M4XOP3JtjXLGo3EnJ1 +pFgdWTOyoSrA8/w1rck4c/ISXZSinVAggPxmLwVEAAln6Itj6giIZHKvA2fL2o8z +CeK057Lu8X6u2CG8tRWSQzVoKIQw/PKK6CNXCAy8vo4EkXudRutnEYHEJlPkVgPn +2qP06GI+I+9zKE37iqj0k1/wFaCVXHXIvn06YrmjQw6I0dDj/60Wvi500FuRVpn9 +twIDAQAB +-----END PUBLIC KEY-----` ) diff --git a/go.mod b/go.mod index b1f0f8fda..3bffc09ed 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,15 @@ module github.com/arduino/arduino-create-agent -go 1.21 +go 1.23.0 require ( fyne.io/systray v1.10.0 - github.com/ProtonMail/go-crypto v1.1.0-alpha.2 - github.com/arduino/go-paths-helper v1.12.0 + github.com/ProtonMail/go-crypto v1.1.0-alpha.5-proton + github.com/arduino/go-paths-helper v1.12.1 github.com/arduino/go-serial-utils v0.1.2 - github.com/arduino/pluggable-discovery-protocol-handler/v2 v2.2.0 + github.com/arduino/pluggable-discovery-protocol-handler/v2 v2.2.1 github.com/blang/semver v3.5.1+incompatible - github.com/codeclysm/extract/v3 v3.1.1 + github.com/codeclysm/extract/v4 v4.0.0 github.com/gin-contrib/cors v1.7.2 github.com/gin-gonic/gin v1.10.0 github.com/go-ini/ini v1.62.0 @@ -21,9 +21,9 @@ require ( github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/stretchr/testify v1.9.0 github.com/xrash/smetrics v0.0.0-20170218160415-a3153f7040e9 - go.bug.st/serial v1.6.1 + go.bug.st/serial v1.6.4 goa.design/goa/v3 v3.16.1 - golang.org/x/sys v0.20.0 + golang.org/x/sys v0.23.0 gopkg.in/inconshreveable/go-update.v0 v0.0.0-20150814200126-d8b0b1d421aa ) @@ -71,7 +71,7 @@ require ( github.com/tevino/abool v1.2.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go v1.1.6 // indirect - github.com/ulikunitz/xz v0.5.11 // indirect + github.com/ulikunitz/xz v0.5.12 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.23.0 // indirect golang.org/x/mod v0.17.0 // indirect diff --git a/go.sum b/go.sum index 278446a66..676649d56 100644 --- a/go.sum +++ b/go.sum @@ -2,17 +2,17 @@ fyne.io/systray v1.10.0 h1:Yr1D9Lxeiw3+vSuZWPlaHC8BMjIHZXJKkek706AfYQk= fyne.io/systray v1.10.0/go.mod h1:oM2AQqGJ1AMo4nNqZFYU8xYygSBZkW2hmdJ7n4yjedE= github.com/AnatolyRugalev/goregen v0.1.0 h1:xrdXkLaskMnbxW0x4FWNj2yoednv0X2bcTBWpuJGYfE= github.com/AnatolyRugalev/goregen v0.1.0/go.mod h1:sVlY1tjcirqLBRZnCcIq1+7/Lwmqz5g7IK8AStjOVzI= -github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg= -github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/ProtonMail/go-crypto v1.1.0-alpha.5-proton h1:KVBEgU3CJpmzLChnLiSuEyCuhGhcMt3eOST+7A+ckto= +github.com/ProtonMail/go-crypto v1.1.0-alpha.5-proton/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/arduino/go-paths-helper v1.0.1/go.mod h1:HpxtKph+g238EJHq4geEPv9p+gl3v5YYu35Yb+w31Ck= -github.com/arduino/go-paths-helper v1.12.0 h1:xizOQtI9iHdl19qXd1EmWg5i9W//2bOCOYwlNv8F61E= -github.com/arduino/go-paths-helper v1.12.0/go.mod h1:jcpW4wr0u69GlXhTYydsdsqAjLaYK5n7oWHfKqOG6LM= +github.com/arduino/go-paths-helper v1.12.1 h1:WkxiVUxBjKWlLMiMuYy8DcmVrkxdP7aKxQOAq7r2lVM= +github.com/arduino/go-paths-helper v1.12.1/go.mod h1:jcpW4wr0u69GlXhTYydsdsqAjLaYK5n7oWHfKqOG6LM= github.com/arduino/go-properties-orderedmap v1.8.0 h1:wEfa6hHdpezrVOh787OmClsf/Kd8qB+zE3P2Xbrn0CQ= github.com/arduino/go-properties-orderedmap v1.8.0/go.mod h1:DKjD2VXY/NZmlingh4lSFMEYCVubfeArCsGPGDwb2yk= github.com/arduino/go-serial-utils v0.1.2 h1:MRFwME4w/uaVkJ1R+wzz4KSbI9cF9IDVrYorazvjpTk= github.com/arduino/go-serial-utils v0.1.2/go.mod h1:kzIsNPgz8DFAd1sAFKve4ubxrdGcwQ4XzvRLlztsgnE= -github.com/arduino/pluggable-discovery-protocol-handler/v2 v2.2.0 h1:v7og6LpskewFabmaShKVzWXl5MXbmsxaRP3yo4dJta8= -github.com/arduino/pluggable-discovery-protocol-handler/v2 v2.2.0/go.mod h1:1dgblsmK2iBx3L5iNTyRIokeaxbTLUrYiUbHBK6yC3Y= +github.com/arduino/pluggable-discovery-protocol-handler/v2 v2.2.1 h1:Fw8zKj1b/FkcQrWgN7aBw3ubSxpKIUtdANLXvd1Qdzw= +github.com/arduino/pluggable-discovery-protocol-handler/v2 v2.2.1/go.mod h1:1dgblsmK2iBx3L5iNTyRIokeaxbTLUrYiUbHBK6yC3Y= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= @@ -25,8 +25,8 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/codeclysm/extract/v3 v3.1.1 h1:iHZtdEAwSTqPrd+1n4jfhr1qBhUWtHlMTjT90+fJVXg= -github.com/codeclysm/extract/v3 v3.1.1/go.mod h1:ZJi80UG2JtfHqJI+lgJSCACttZi++dHxfWuPaMhlOfQ= +github.com/codeclysm/extract/v4 v4.0.0 h1:H87LFsUNaJTu2e/8p/oiuiUsOK/TaPQ5wxsjPnwPEIY= +github.com/codeclysm/extract/v4 v4.0.0/go.mod h1:SFju1lj6as7FvUgalpSct7torJE0zttbJUWtryPRG6s= github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -155,12 +155,12 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go v1.1.6 h1:zoJUBK8kLIUDNJ9LGB04qOQRfoDRoWBBpl0X7pZyi5I= github.com/ugorji/go v1.1.6/go.mod h1:RaaajvHwnCbhlqWLTIB78hyPWp24YUXhQ3YXM7Hg7os= -github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= -github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= +github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/xrash/smetrics v0.0.0-20170218160415-a3153f7040e9 h1:w8V9v0qVympSF6GjdjIyeqR7+EVhAF9CBQmkmW7Zw0w= github.com/xrash/smetrics v0.0.0-20170218160415-a3153f7040e9/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -go.bug.st/serial v1.6.1 h1:VSSWmUxlj1T/YlRo2J104Zv3wJFrjHIl/T3NeruWAHY= -go.bug.st/serial v1.6.1/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= +go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= +go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= goa.design/goa/v3 v3.16.1 h1:yZwbKrfMpE8+sz0uf+n+BtArVOFQ0kNSC0twQKwQb04= goa.design/goa/v3 v3.16.1/go.mod h1:Yd42LR0PYDbHSbsbF3vNd4YY/O+LG20Jb7+IyNdkQic= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= @@ -181,8 +181,8 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/home.html b/home.html index e66b7c975..ee523981a 100644 --- a/home.html +++ b/home.html @@ -1,321 +1,346 @@ - - - Arduino Create Agent Debug Console - - - + + Arduino Cloud Agent Debug Console + + + - - + - +
-
-

-            

-        
- - - - \ No newline at end of file + + diff --git a/main.go b/main.go index 1ca857b02..41f824b1b 100755 --- a/main.go +++ b/main.go @@ -81,7 +81,7 @@ var ( logDump = iniConf.String("log", "off", "off = (default)") origins = iniConf.String("origins", "", "Allowed origin list for CORS") portsFilterRegexp = iniConf.String("regex", "usb|acm|com", "Regular expression to filter serial port list") - signatureKey = iniConf.String("signatureKey", globals.SignatureKey, "Pem-encoded public key to verify signed commandlines") + signatureKey = iniConf.String("signatureKey", globals.ArduinoSignaturePubKey, "Pem-encoded public key to verify signed commandlines") updateURL = iniConf.String("updateUrl", "", "") verbose = iniConf.Bool("v", true, "show debug logging") crashreport = iniConf.Bool("crashreport", false, "enable crashreport logging") @@ -278,9 +278,17 @@ func loop() { } } + if signatureKey == nil || len(*signatureKey) == 0 { + log.Panicf("signature public key should be set") + } + signaturePubKey, err := utilities.ParseRsaPublicKey([]byte(*signatureKey)) + if err != nil { + log.Panicf("cannot parse signature key '%s'. %s", *signatureKey, err) + } + // Instantiate Index and Tools Index = index.Init(*indexURL, config.GetDataDir()) - Tools = tools.New(config.GetDataDir(), Index, logger) + Tools = tools.New(config.GetDataDir(), Index, logger, signaturePubKey) // see if we are supposed to wait 5 seconds if *isLaunchSelf { @@ -454,7 +462,7 @@ func loop() { r.LoadHTMLFiles("templates/nofirefox.html") r.GET("/", homeHandler) - r.POST("/upload", uploadHandler) + r.POST("/upload", uploadHandler(signaturePubKey)) r.GET("/socket.io/", socketHandler) r.POST("/socket.io/", socketHandler) r.Handle("WS", "/socket.io/", socketHandler) @@ -464,7 +472,7 @@ func loop() { r.POST("/update", updateHandler) // Mount goa handlers - goa := v2.Server(config.GetDataDir().String(), Index) + goa := v2.Server(config.GetDataDir().String(), Index, signaturePubKey) r.Any("/v2/*path", gin.WrapH(goa)) go func() { diff --git a/main_test.go b/main_test.go index c8276bba8..1387fd221 100644 --- a/main_test.go +++ b/main_test.go @@ -18,6 +18,7 @@ package main import ( "bytes" "crypto/x509" + "encoding/base64" "encoding/json" "encoding/pem" "fmt" @@ -29,8 +30,10 @@ import ( "github.com/arduino/arduino-create-agent/config" "github.com/arduino/arduino-create-agent/gen/tools" + "github.com/arduino/arduino-create-agent/globals" "github.com/arduino/arduino-create-agent/index" "github.com/arduino/arduino-create-agent/upload" + "github.com/arduino/arduino-create-agent/utilities" v2 "github.com/arduino/arduino-create-agent/v2" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" @@ -53,7 +56,7 @@ func TestValidSignatureKey(t *testing.T) { func TestUploadHandlerAgainstEvilFileNames(t *testing.T) { r := gin.New() - r.POST("/", uploadHandler) + r.POST("/", uploadHandler(utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey)))) ts := httptest.NewServer(r) uploadEvilFileName := Upload{ @@ -87,6 +90,30 @@ func TestUploadHandlerAgainstEvilFileNames(t *testing.T) { } } +func TestUploadHandlerAgainstBase64WithoutPaddingMustFail(t *testing.T) { + r := gin.New() + r.POST("/", uploadHandler(utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey)))) + ts := httptest.NewServer(r) + defer ts.Close() + + // When calling the `BindJSON` func, when a json field will be Unmarshaled + // in a []byte type, we expect to receive a base64 padded string in input. + // In case we receive a base64 unpadded string BindJSON fails. + // The expectation here is that the upload handler won't continue with the + // upload operation. + base64ContentWithoutPadding := base64.RawStdEncoding.EncodeToString([]byte("test")) + payload := fmt.Sprintf(`{"hex": "%s"}`, base64ContentWithoutPadding) + + resp, err := http.Post(ts.URL, "encoding/json", bytes.NewBufferString(payload)) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "err with the payload. illegal base64 data at input") +} + func TestInstallToolV2(t *testing.T) { indexURL := "https://downloads.arduino.cc/packages/package_index.json" @@ -94,7 +121,7 @@ func TestInstallToolV2(t *testing.T) { Index := index.Init(indexURL, config.GetDataDir()) r := gin.New() - goa := v2.Server(config.GetDataDir().String(), Index) + goa := v2.Server(config.GetDataDir().String(), Index, utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))) r.Any("/v2/*path", gin.WrapH(goa)) ts := httptest.NewServer(r) @@ -116,6 +143,18 @@ func TestInstallToolV2(t *testing.T) { Signature: &bossacSignature, } + esptoolURL := "https://github.com/earlephilhower/esp-quick-toolchain/releases/download/2.5.0-3/x86_64-linux-gnu.esptool-f80ae31.tar.gz" + esptoolChecksum := "SHA-256:bded1dca953377838b6086a9bcd40a1dc5286ba5f69f2372c22a1d1819baad24" + esptoolSignature := "852b58871419ce5e5633ecfaa72c0f0fa890ceb51164b362b8133bc0e3e003a21cec48935b8cdc078f4031219cbf17fb7edd9d7c9ca8ed85492911c9ca6353c9aa4691eb91fda99563a6bd49aeca0d9981fb05ec76e45c6024f8a6822862ad1e34ddc652fbbf4fa909887a255d4f087398ec386577efcec523c21203be3d10fc9e9b0f990a7536875a77dc2bc5cbffea7734b62238e31719111b718bacccebffc9be689545540e81d23b81caa66214376f58a0d6a45cf7efc5d3af62ab932b371628162fffe403906f41d5534921e5be081c5ac2ecc9db5caec03a105cc44b00ce19a95ad079843501eb8182e0717ce327867380c0e39d2b48698547fc1d0d66" + esptoolInstallURLOK := tools.ToolPayload{ + Name: "esptool", + Version: "2.5.0-3-20ed2b9", + Packager: "esp8266", + URL: &esptoolURL, + Checksum: &esptoolChecksum, + Signature: &esptoolSignature, + } + wrongSignature := "wr0ngs1gn4tur3" bossacInstallWrongSig := tools.ToolPayload{ Name: "bossac", @@ -147,6 +186,7 @@ func TestInstallToolV2(t *testing.T) { {bossacInstallWrongSig, http.StatusInternalServerError, "verification error"}, {bossacInstallWrongCheck, http.StatusInternalServerError, "checksum of downloaded file doesn't match"}, {bossacInstallNoURL, http.StatusOK, "ok"}, + {esptoolInstallURLOK, http.StatusOK, "ok"}, } for _, test := range tests { @@ -175,7 +215,7 @@ func TestInstalledHead(t *testing.T) { Index := index.Init(indexURL, config.GetDataDir()) r := gin.New() - goa := v2.Server(config.GetDataDir().String(), Index) + goa := v2.Server(config.GetDataDir().String(), Index, utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))) r.Any("/v2/*path", gin.WrapH(goa)) ts := httptest.NewServer(r) diff --git a/serial.go b/serial.go index 64e5b8f7f..1a43f3644 100755 --- a/serial.go +++ b/serial.go @@ -31,7 +31,7 @@ import ( type serialhub struct { // Opened serial ports. - ports map[*serport]bool + ports map[string]*serport mu sync.Mutex } @@ -60,7 +60,7 @@ type SpPortItem struct { var serialPorts SerialPortList var sh = serialhub{ - ports: make(map[*serport]bool), + ports: make(map[string]*serport), } // Register serial ports from the connections. @@ -68,7 +68,7 @@ func (sh *serialhub) Register(port *serport) { sh.mu.Lock() //log.Print("Registering a port: ", p.portConf.Name) h.broadcastSys <- []byte("{\"Cmd\":\"Open\",\"Desc\":\"Got register/open on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + ",\"BufferType\":\"" + port.BufferType + "\"}") - sh.ports[port] = true + sh.ports[port.portName] = port sh.mu.Unlock() } @@ -77,7 +77,7 @@ func (sh *serialhub) Unregister(port *serport) { sh.mu.Lock() //log.Print("Unregistering a port: ", p.portConf.Name) h.broadcastSys <- []byte("{\"Cmd\":\"Close\",\"Desc\":\"Got unregister/close on port.\",\"Port\":\"" + port.portConf.Name + "\",\"Baud\":" + strconv.Itoa(port.portConf.Baud) + "}") - delete(sh.ports, port) + delete(sh.ports, port.portName) close(port.sendBuffered) close(port.sendNoBuf) sh.mu.Unlock() @@ -86,15 +86,8 @@ func (sh *serialhub) Unregister(port *serport) { func (sh *serialhub) FindPortByName(portname string) (*serport, bool) { sh.mu.Lock() defer sh.mu.Unlock() - - for port := range sh.ports { - if strings.EqualFold(port.portConf.Name, portname) { - // we found our port - //spHandlerClose(port) - return port, true - } - } - return nil, false + port, ok := sh.ports[portname] + return port, ok } // List broadcasts a Json representation of the ports found diff --git a/serialport.go b/serialport.go index a11483f63..b3418fe5d 100755 --- a/serialport.go +++ b/serialport.go @@ -20,6 +20,8 @@ import ( "encoding/base64" "io" "strconv" + "sync" + "sync/atomic" "time" "unicode/utf8" @@ -43,7 +45,7 @@ type serport struct { // Keep track of whether we're being actively closed // just so we don't show scary error messages - isClosing bool + isClosing atomic.Bool isClosingDueToError bool @@ -85,7 +87,7 @@ func (p *serport) reader(buftype string) { bufferPart := serialBuffer[:n] //if we detect that port is closing, break out of this for{} loop. - if p.isClosing { + if p.isClosing.Load() { strmsg := "Shutting down reader on " + p.portConf.Name log.Println(strmsg) h.broadcastSys <- []byte(strmsg) @@ -272,7 +274,13 @@ func (p *serport) writerRaw() { h.broadcastSys <- []byte(msgstr) } +// This lock is used to prevent multiple threads from trying to open the same port at the same time. +// It presents issues with the serial port driver on some OS's: https://github.com/arduino/arduino-create-agent/issues/1031 +var spHandlerOpenLock sync.Mutex + func spHandlerOpen(portname string, baud int, buftype string) { + spHandlerOpenLock.Lock() + defer spHandlerOpenLock.Unlock() log.Print("Inside spHandler") @@ -294,11 +302,14 @@ func spHandlerOpen(portname string, baud int, buftype string) { sp, err := serial.Open(portname, mode) log.Print("Just tried to open port") if err != nil { - //log.Fatal(err) - log.Print("Error opening port " + err.Error()) - //h.broadcastSys <- []byte("Error opening port. " + err.Error()) - h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Error opening port. " + err.Error() + "\",\"Port\":\"" + conf.Name + "\",\"Baud\":" + strconv.Itoa(conf.Baud) + "}") - + existingPort, ok := sh.FindPortByName(portname) + if ok && existingPort.portConf.Baud == baud && existingPort.BufferType == buftype { + log.Print("Port already opened") + h.broadcastSys <- []byte("{\"Cmd\":\"Open\",\"Desc\":\"Port already opened.\",\"Port\":\"" + existingPort.portConf.Name + "\",\"Baud\":" + strconv.Itoa(existingPort.portConf.Baud) + ",\"BufferType\":\"" + existingPort.BufferType + "\"}") + } else { + log.Print("Error opening port " + err.Error()) + h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Error opening port. " + err.Error() + "\",\"Port\":\"" + conf.Name + "\",\"Baud\":" + strconv.Itoa(conf.Baud) + "}") + } return } log.Print("Opened port successfully") @@ -330,7 +341,6 @@ func spHandlerOpen(portname string, baud int, buftype string) { p.bufferwatcher = bw sh.Register(p) - defer sh.Unregister(p) serialPorts.MarkPortAsOpened(portname) serialPorts.List() @@ -341,14 +351,17 @@ func spHandlerOpen(portname string, baud int, buftype string) { go p.writerNoBuf() // this is thread to send to serial port but with base64 decoding go p.writerRaw() - - p.reader(buftype) - - serialPorts.List() + // this is the thread that reads from the serial port + go func() { + p.reader(buftype) + serialPorts.List() + sh.Unregister(p) + }() } func (p *serport) Close() { - p.isClosing = true + p.isClosing.Store(true) + p.bufferwatcher.Close() p.portIo.Close() serialPorts.MarkPortAsClosed(p.portName) diff --git a/tests/conftest.py b/tests/conftest.py index 622965b64..0e81d76f0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,9 +27,9 @@ @pytest.fixture(scope="function") def agent(pytestconfig): if platform.system() == "Windows": - agent = str(Path(pytestconfig.rootdir) / "arduino-create-agent_cli.exe") + agent = str(Path(pytestconfig.rootdir) / "arduino-cloud-agent_cli.exe") else: - agent = str(Path(pytestconfig.rootdir) / "arduino-create-agent") + agent = str(Path(pytestconfig.rootdir) / "arduino-cloud-agent") env = { # "ARDUINO_DATA_DIR": data_dir, # "ARDUINO_DOWNLOADS_DIR": downloads_dir, diff --git a/tools/download.go b/tools/download.go index 360d6e4c3..8c4a37a6c 100644 --- a/tools/download.go +++ b/tools/download.go @@ -16,43 +16,17 @@ package tools import ( - "bytes" "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" "errors" - "fmt" - "io" - "net/http" "os" "os/exec" "path/filepath" "runtime" - "github.com/arduino/arduino-create-agent/v2/pkgs" - "github.com/arduino/go-paths-helper" - "github.com/blang/semver" - "github.com/codeclysm/extract/v3" + "github.com/arduino/arduino-create-agent/gen/tools" + "github.com/arduino/arduino-create-agent/utilities" ) -// public vars to allow override in the tests -var ( - OS = runtime.GOOS - Arch = runtime.GOARCH -) - -func pathExists(path string) bool { - _, err := os.Stat(path) - if err == nil { - return true - } - if os.IsNotExist(err) { - return false - } - return true -} - // Download will parse the index at the indexURL for the tool to download. // It will extract it in a folder in .arduino-create, and it will update the // Installed map. @@ -70,97 +44,21 @@ func pathExists(path string) bool { // if it already exists. func (t *Tools) Download(pack, name, version, behaviour string) error { - body, err := t.index.Read() - if err != nil { - return err - } - - var data pkgs.Index - json.Unmarshal(body, &data) - - // Find the tool by name - correctTool, correctSystem := findTool(pack, name, version, data) - - if correctTool.Name == "" || correctSystem.URL == "" { - t.logger("We couldn't find a tool with the name " + name + " and version " + version + " packaged by " + pack) - return nil - } - - key := correctTool.Name + "-" + correctTool.Version - - // Check if it already exists - if behaviour == "keep" { - location, ok := t.getMapValue(key) - if ok && pathExists(location) { - // overwrite the default tool with this one - t.setMapValue(correctTool.Name, location) - t.logger("The tool is already present on the system") - return t.writeMap() - } - } - - // Download the tool - t.logger("Downloading tool " + name + " from " + correctSystem.URL) - resp, err := http.Get(correctSystem.URL) + t.tools.SetBehaviour(behaviour) + _, err := t.tools.Install(context.Background(), &tools.ToolPayload{Name: name, Version: version, Packager: pack}) if err != nil { return err } - defer resp.Body.Close() - - // Read the body - body, err = io.ReadAll(resp.Body) - if err != nil { - return err - } - - // Checksum - checksum := sha256.Sum256(body) - checkSumString := "SHA-256:" + hex.EncodeToString(checksum[:sha256.Size]) - - if checkSumString != correctSystem.Checksum { - return errors.New("checksum doesn't match") - } - - tempPath := paths.TempDir() - // Create a temporary dir to extract package - if err := tempPath.MkdirAll(); err != nil { - return fmt.Errorf("creating temp dir for extraction: %s", err) - } - tempDir, err := tempPath.MkTempDir("package-") - if err != nil { - return fmt.Errorf("creating temp dir for extraction: %s", err) - } - defer tempDir.RemoveAll() - t.logger("Unpacking tool " + name) - ctx := context.Background() - reader := bytes.NewReader(body) - // Extract into temp directory - if err := extract.Archive(ctx, reader, tempDir.String(), nil); err != nil { - return fmt.Errorf("extracting archive: %s", err) - } - - location := t.directory.Join(pack, correctTool.Name, correctTool.Version) - err = location.RemoveAll() + path := filepath.Join(pack, name, version) + safePath, err := utilities.SafeJoin(t.directory.String(), path) if err != nil { return err } - // Check package content and find package root dir - root, err := findPackageRoot(tempDir) - if err != nil { - return fmt.Errorf("searching package root dir: %s", err) - } - - if err := root.Rename(location); err != nil { - if err := root.CopyDirTo(location); err != nil { - return fmt.Errorf("moving extracted archive to destination dir: %s", err) - } - } - // if the tool contains a post_install script, run it: it means it is a tool that needs to install drivers // AFAIK this is only the case for the windows-driver tool - err = t.installDrivers(location.String()) + err = t.installDrivers(safePath) if err != nil { return err } @@ -169,55 +67,12 @@ func (t *Tools) Download(pack, name, version, behaviour string) error { t.logger("Ensure that the files are executable") // Update the tool map - t.logger("Updating map with location " + location.String()) - - t.setMapValue(name, location.String()) - t.setMapValue(name+"-"+correctTool.Version, location.String()) - return t.writeMap() -} + t.logger("Updating map with location " + safePath) -func findPackageRoot(parent *paths.Path) (*paths.Path, error) { - files, err := parent.ReadDir() - if err != nil { - return nil, fmt.Errorf("reading package root dir: %s", err) - } - files.FilterOutPrefix("__MACOSX") + t.setMapValue(name, safePath) + t.setMapValue(name+"-"+version, safePath) - // if there is only one dir, it is the root dir - if len(files) == 1 && files[0].IsDir() { - return files[0], nil - } - return parent, nil -} - -func findTool(pack, name, version string, data pkgs.Index) (pkgs.Tool, pkgs.System) { - var correctTool pkgs.Tool - correctTool.Version = "0.0" - - for _, p := range data.Packages { - if p.Name != pack { - continue - } - for _, t := range p.Tools { - if version != "latest" { - if t.Name == name && t.Version == version { - correctTool = t - } - } else { - // Find latest - v1, _ := semver.Make(t.Version) - v2, _ := semver.Make(correctTool.Version) - if t.Name == name && v1.Compare(v2) > 0 { - correctTool = t - } - } - } - } - - // Find the url based on system - correctSystem := correctTool.GetFlavourCompatibleWith(OS, Arch) - - return correctTool, correctSystem + return nil } func (t *Tools) installDrivers(location string) error { @@ -225,7 +80,7 @@ func (t *Tools) installDrivers(location string) error { extension := ".bat" // add .\ to force locality preamble := ".\\" - if OS != "windows" { + if runtime.GOOS != "windows" { extension = ".sh" // add ./ to force locality preamble = "./" @@ -237,7 +92,7 @@ func (t *Tools) installDrivers(location string) error { os.Chdir(location) t.logger(preamble + "post_install" + extension) oscmd := exec.Command(preamble + "post_install" + extension) - if OS != "linux" { + if runtime.GOOS != "linux" { // spawning a shell could be the only way to let the user type his password TellCommandNotToSpawnShell(oscmd) } diff --git a/tools/download_test.go b/tools/download_test.go index c45914b51..96a105fd7 100644 --- a/tools/download_test.go +++ b/tools/download_test.go @@ -21,7 +21,9 @@ import ( "testing" "time" + "github.com/arduino/arduino-create-agent/globals" "github.com/arduino/arduino-create-agent/index" + "github.com/arduino/arduino-create-agent/utilities" "github.com/arduino/arduino-create-agent/v2/pkgs" "github.com/arduino/go-paths-helper" "github.com/stretchr/testify/require" @@ -42,8 +44,8 @@ func TestDownloadCorrectPlatform(t *testing.T) { {"linux", "arm", "arm-linux-gnueabihf"}, } defer func() { - OS = runtime.GOOS // restore `runtime.OS` - Arch = runtime.GOARCH // restore `runtime.ARCH` + pkgs.OS = runtime.GOOS // restore `runtime.OS` + pkgs.Arch = runtime.GOARCH // restore `runtime.ARCH` }() testIndex := paths.New("testdata", "test_tool_index.json") buf, err := testIndex.ReadFile() @@ -54,10 +56,11 @@ func TestDownloadCorrectPlatform(t *testing.T) { require.NoError(t, err) for _, tc := range testCases { t.Run(tc.hostOS+tc.hostArch, func(t *testing.T) { - OS = tc.hostOS // override `runtime.OS` for testing purposes - Arch = tc.hostArch // override `runtime.ARCH` for testing purposes + pkgs.OS = tc.hostOS // override `runtime.OS` for testing purposes + pkgs.Arch = tc.hostArch // override `runtime.ARCH` for testing purposes // Find the tool by name - correctTool, correctSystem := findTool("arduino-test", "arduino-fwuploader", "2.2.2", data) + correctTool, correctSystem, found := pkgs.FindTool("arduino-test", "arduino-fwuploader", "2.2.2", data) + require.True(t, found) require.NotNil(t, correctTool) require.NotNil(t, correctSystem) require.Equal(t, correctTool.Name, "arduino-fwuploader") @@ -78,8 +81,8 @@ func TestDownloadFallbackPlatform(t *testing.T) { {"windows", "amd64", "i686-mingw32"}, } defer func() { - OS = runtime.GOOS // restore `runtime.OS` - Arch = runtime.GOARCH // restore `runtime.ARCH` + pkgs.OS = runtime.GOOS // restore `runtime.OS` + pkgs.Arch = runtime.GOARCH // restore `runtime.ARCH` }() testIndex := paths.New("testdata", "test_tool_index.json") buf, err := testIndex.ReadFile() @@ -90,10 +93,11 @@ func TestDownloadFallbackPlatform(t *testing.T) { require.NoError(t, err) for _, tc := range testCases { t.Run(tc.hostOS+tc.hostArch, func(t *testing.T) { - OS = tc.hostOS // override `runtime.OS` for testing purposes - Arch = tc.hostArch // override `runtime.ARCH` for testing purposes + pkgs.OS = tc.hostOS // override `runtime.OS` for testing purposes + pkgs.Arch = tc.hostArch // override `runtime.ARCH` for testing purposes // Find the tool by name - correctTool, correctSystem := findTool("arduino-test", "arduino-fwuploader", "2.2.0", data) + correctTool, correctSystem, found := pkgs.FindTool("arduino-test", "arduino-fwuploader", "2.2.0", data) + require.True(t, found) require.NotNil(t, correctTool) require.NotNil(t, correctSystem) require.Equal(t, correctTool.Name, "arduino-fwuploader") @@ -118,7 +122,6 @@ func TestDownload(t *testing.T) { {"rp2040tools", "1.0.6", []string{"elf2uf2", "picotool", "pioasm", "rp2040load"}}, {"esptool_py", "4.5.1", []string{"esptool"}}, {"arduino-fwuploader", "2.2.2", []string{"arduino-fwuploader"}}, - {"fwupdater", "0.1.12", []string{"firmwares", "FirmwareUploader"}}, // old legacy tool } // prepare the test environment tempDir := t.TempDir() @@ -127,7 +130,7 @@ func TestDownload(t *testing.T) { IndexFile: *paths.New("testdata", "test_tool_index.json"), LastRefresh: time.Now(), } - testTools := New(tempDirPath, &testIndex, func(msg string) { t.Log(msg) }) + testTools := New(tempDirPath, &testIndex, func(msg string) { t.Log(msg) }, utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))) for _, tc := range testCases { t.Run(tc.name+"-"+tc.version, func(t *testing.T) { @@ -145,7 +148,7 @@ func TestDownload(t *testing.T) { if filePath.IsDir() { require.DirExists(t, filePath.String()) } else { - if OS == "windows" { + if runtime.GOOS == "windows" { require.FileExists(t, filePath.String()+".exe") } else { require.FileExists(t, filePath.String()) @@ -159,3 +162,23 @@ func TestDownload(t *testing.T) { }) } } + +func TestCorruptedInstalled(t *testing.T) { + // prepare the test environment + tempDir := t.TempDir() + tempDirPath := paths.New(tempDir) + testIndex := index.Resource{ + IndexFile: *paths.New("testdata", "test_tool_index.json"), + LastRefresh: time.Now(), + } + corruptedJSON := tempDirPath.Join("installed.json") + fileJSON, err := corruptedJSON.Create() + require.NoError(t, err) + defer fileJSON.Close() + _, err = fileJSON.Write([]byte("Hello")) + require.NoError(t, err) + testTools := New(tempDirPath, &testIndex, func(msg string) { t.Log(msg) }, utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))) + // Download the tool + err = testTools.Download("arduino-test", "avrdude", "6.3.0-arduino17", "keep") + require.NoError(t, err) +} diff --git a/tools/testdata/test_tool_index.json b/tools/testdata/test_tool_index.json index fdee1cc9d..b7f8b87d2 100644 --- a/tools/testdata/test_tool_index.json +++ b/tools/testdata/test_tool_index.json @@ -514,61 +514,6 @@ "size": "6829396" } ] - }, - { - "name": "fwupdater", - "version": "0.1.12", - "systems": [ - { - "host": "i686-linux-gnu", - "url": "http://downloads.arduino.cc/tools/FirmwareUploader_0.1.12_Linux_32bit.tar.bz2", - "archiveFileName": "FirmwareUploader_0.1.12_Linux_32bit.tar.bz2", - "checksum": "SHA-256:2fec2bdfd20ad4950bc9ba37108dc2a7c152f569174279c0697efe1f5a0db781", - "size": "26097546" - }, - { - "host": "x86_64-pc-linux-gnu", - "url": "http://downloads.arduino.cc/tools/FirmwareUploader_0.1.12_Linux_64bit.tar.bz2", - "archiveFileName": "FirmwareUploader_0.1.12_Linux_64bit.tar.bz2", - "checksum": "SHA-256:ce57d0afef30cb7d3513f5da326346c99d6bf4923bbc2200634086811f3fb31e", - "size": "26073327" - }, - { - "host": "i686-mingw32", - "url": "http://downloads.arduino.cc/tools/FirmwareUploader_0.1.12_Windows_32bit.zip", - "archiveFileName": "FirmwareUploader_0.1.12_Windows_32bit.zip", - "checksum": "SHA-256:558568b453caa1c821def8cc6d34555d0c910eb7e7e871de3ae1c39ae6f01bdd", - "size": "25743641" - }, - { - "host": "x86_64-mingw32", - "url": "http://downloads.arduino.cc/tools/FirmwareUploader_0.1.12_Windows_64bit.zip", - "archiveFileName": "FirmwareUploader_0.1.12_Windows_64bit.zip", - "checksum": "SHA-256:ec16de33620985434280c92c3c322257b89bb67adf8fd4d5dd5f9467ea1e9e40", - "size": "25851428" - }, - { - "host": "i386-apple-darwin11", - "url": "http://downloads.arduino.cc/tools/FirmwareUploader_0.1.12_macOS_64bit.tar.bz2", - "archiveFileName": "FirmwareUploader_0.1.12_macOS_64bit.tar.bz2", - "checksum": "SHA-256:a470361b57f86ddfcaecd274d844af51ee1d23a71cd6c26e30fcef2152d1a03f", - "size": "25792860" - }, - { - "host": "arm-linux-gnueabihf", - "url": "http://downloads.arduino.cc/tools/FirmwareUploader_0.1.12_Linux_ARM.tar.bz2", - "archiveFileName": "FirmwareUploader_0.1.12_Linux_ARM.tar.bz2", - "checksum": "SHA-256:855fa0a9b942c3ee18906efc510bdfe30bf3334ff28ffbb476e648ff30033847", - "size": "25936245" - }, - { - "host": "aarch64-linux-gnu", - "url": "http://downloads.arduino.cc/tools/FirmwareUploader_0.1.12_Linux_ARM64.tar.bz2", - "archiveFileName": "FirmwareUploader_0.1.12_Linux_ARM64.tar.bz2", - "checksum": "SHA-256:691461e64fe075e9a79801347c2bd895fb72f8f2c45a7cd49056c6ad9efe8fc4", - "size": "25967430" - } - ] } ] } diff --git a/tools/tools.go b/tools/tools.go index e641db351..f371126b5 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -16,12 +16,14 @@ package tools import ( + "crypto/rsa" "encoding/json" "path/filepath" "strings" "sync" "github.com/arduino/arduino-create-agent/index" + "github.com/arduino/arduino-create-agent/v2/pkgs" "github.com/arduino/go-paths-helper" "github.com/xrash/smetrics" ) @@ -47,19 +49,21 @@ type Tools struct { logger func(msg string) installed map[string]string mutex sync.RWMutex + tools *pkgs.Tools } // New will return a Tool object, allowing the caller to execute operations on it. // The New functions accept the directory to use to host the tools, // an index (used to download the tools), // and a logger to log the operations -func New(directory *paths.Path, index *index.Resource, logger func(msg string)) *Tools { +func New(directory *paths.Path, index *index.Resource, logger func(msg string), signPubKey *rsa.PublicKey) *Tools { t := &Tools{ directory: directory, index: index, logger: logger, installed: map[string]string{}, mutex: sync.RWMutex{}, + tools: pkgs.New(index, directory.String(), "replace", signPubKey), } _ = t.readMap() return t @@ -78,18 +82,6 @@ func (t *Tools) getMapValue(key string) (string, bool) { return value, ok } -// writeMap() writes installed map to the json file "installed.json" -func (t *Tools) writeMap() error { - t.mutex.RLock() - defer t.mutex.RUnlock() - b, err := json.Marshal(t.installed) - if err != nil { - return err - } - filePath := t.directory.Join("installed.json") - return filePath.WriteFile(b) -} - // readMap() reads the installed map from json file "installed.json" func (t *Tools) readMap() error { t.mutex.Lock() diff --git a/updater/updater_darwin.go b/updater/updater_darwin.go index ec00f88cc..829466352 100644 --- a/updater/updater_darwin.go +++ b/updater/updater_darwin.go @@ -26,7 +26,7 @@ import ( "strings" "github.com/arduino/go-paths-helper" - "github.com/codeclysm/extract/v3" + "github.com/codeclysm/extract/v4" "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus" ) @@ -143,7 +143,9 @@ func checkForUpdates(currentVersion string, updateURL string, cmdName string) (s // Install new app logrus.WithField("from", tmpAppPath).WithField("to", currentAppPath).Info("Copying updated app") - if err := tmpAppPath.CopyDirTo(currentAppPath); err != nil { + createPath := currentAppPath.Join("Contents", "MacOS", "Arduino_Create_Agent") + cloudPath := currentAppPath.Join("Contents", "MacOS", "Arduino_Cloud_Agent") + if err := tmpAppPath.CopyDirTo(currentAppPath); err != nil || (!createPath.Exist() && !cloudPath.Exist()) { // Try rollback changes _ = currentAppPath.RemoveAll() _ = oldAppPath.Rename(currentAppPath) diff --git a/utilities/utilities.go b/utilities/utilities.go index 5979732d4..662672da7 100644 --- a/utilities/utilities.go +++ b/utilities/utilities.go @@ -30,8 +30,6 @@ import ( "os/exec" "path/filepath" "strings" - - "github.com/arduino/arduino-create-agent/globals" ) // SaveFileonTempDir creates a temp directory and saves the file data as the @@ -131,23 +129,44 @@ func SafeJoin(parent, subdir string) (string, error) { return res, nil } -// VerifyInput will verify an input against a signature +// VerifyInput will verify an input against a signature using the public key. // A valid signature is indicated by returning a nil error. -func VerifyInput(input string, signature string) error { +func VerifyInput(input string, signature string, pubKey *rsa.PublicKey) error { sign, _ := hex.DecodeString(signature) - block, _ := pem.Decode([]byte(globals.SignatureKey)) + h := sha256.New() + h.Write([]byte(input)) + d := h.Sum(nil) + return rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, d, sign) +} + +// ParseRsaPublicKey parses a public key in PEM format and returns the rsa.PublicKey object. +// Returns an error if the key is invalid. +func ParseRsaPublicKey(key []byte) (*rsa.PublicKey, error) { + block, _ := pem.Decode(key) if block == nil { - return errors.New("invalid key") + return nil, errors.New("invalid key") } - key, err := x509.ParsePKIXPublicKey(block.Bytes) + + parsedKey, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { - return err + return nil, err } - rsaKey := key.(*rsa.PublicKey) - h := sha256.New() - h.Write([]byte(input)) - d := h.Sum(nil) - return rsa.VerifyPKCS1v15(rsaKey, crypto.SHA256, d, sign) + + publicKey, ok := parsedKey.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("not an rsa key") + } + return publicKey, nil +} + +// MustParseRsaPublicKey parses a public key in PEM format and returns the rsa.PublicKey object. +// Panics if the key is invalid. +func MustParseRsaPublicKey(key []byte) *rsa.PublicKey { + parsedKey, err := ParseRsaPublicKey(key) + if err != nil { + panic(err) + } + return parsedKey } // UserPrompt executes an osascript and returns the pressed button diff --git a/v2/http.go b/v2/http.go index bcfbc82aa..2b47b93a8 100644 --- a/v2/http.go +++ b/v2/http.go @@ -17,6 +17,7 @@ package v2 import ( "context" + "crypto/rsa" "encoding/json" "net/http" @@ -31,7 +32,7 @@ import ( ) // Server is the actual server -func Server(directory string, index *index.Resource) http.Handler { +func Server(directory string, index *index.Resource, pubKey *rsa.PublicKey) http.Handler { mux := goahttp.NewMuxer() // Instantiate logger @@ -40,7 +41,7 @@ func Server(directory string, index *index.Resource) http.Handler { logAdapter := LogAdapter{Logger: logger} // Mount tools - toolsSvc := pkgs.New(index, directory) + toolsSvc := pkgs.New(index, directory, "replace", pubKey) toolsEndpoints := toolssvc.NewEndpoints(toolsSvc) toolsServer := toolssvr.New(toolsEndpoints, mux, CustomRequestDecoder, goahttp.ResponseEncoder, errorHandler(logger), nil) toolssvr.Mount(mux, toolsServer) diff --git a/v2/pkgs/pkgs.go b/v2/pkgs/pkgs.go index f4965117c..07e392b2f 100644 --- a/v2/pkgs/pkgs.go +++ b/v2/pkgs/pkgs.go @@ -50,7 +50,7 @@ type System struct { Checksum string `json:"checksum"` } -// Source: https://github.com/arduino/arduino-cli/blob/master/arduino/cores/tools.go#L129-L142 +// Source: https://github.com/arduino/arduino-cli/blob/master/internal/arduino/cores/tools.go#L129-L142 var ( regexpLinuxArm = regexp.MustCompile("arm.*-linux-gnueabihf") regexpLinuxArm64 = regexp.MustCompile("(aarch64|arm64)-linux-gnu") @@ -66,7 +66,7 @@ var ( regexpFreeBSD64 = regexp.MustCompile("amd64-freebsd[0-9]*") ) -// Source: https://github.com/arduino/arduino-cli/blob/master/arduino/cores/tools.go#L144-L176 +// Source: https://github.com/arduino/arduino-cli/blob/master/internal/arduino/cores/tools.go#L144-L176 func (s *System) isExactMatchWith(osName, osArch string) bool { if s.Host == "all" { return true @@ -101,7 +101,7 @@ func (s *System) isExactMatchWith(osName, osArch string) bool { return false } -// Source: https://github.com/arduino/arduino-cli/blob/master/arduino/cores/tools.go#L178-L198 +// Source: https://github.com/arduino/arduino-cli/blob/master/internal/arduino/cores/tools.go#L178-L198 func (s *System) isCompatibleWith(osName, osArch string) (bool, int) { if s.isExactMatchWith(osName, osArch) { return true, 1000 @@ -125,7 +125,7 @@ func (s *System) isCompatibleWith(osName, osArch string) (bool, int) { } // GetFlavourCompatibleWith returns the downloadable resource (System) compatible with the specified OS/Arch -// Source: https://github.com/arduino/arduino-cli/blob/master/arduino/cores/tools.go#L206-L216 +// Source: https://github.com/arduino/arduino-cli/blob/master/internal/arduino/cores/tools.go#L206-L216 func (t *Tool) GetFlavourCompatibleWith(osName, osArch string) System { var correctSystem System maxSimilarity := -1 diff --git a/v2/pkgs/testdata/test_tool_index.json b/v2/pkgs/testdata/test_tool_index.json index fdee1cc9d..b7f8b87d2 100644 --- a/v2/pkgs/testdata/test_tool_index.json +++ b/v2/pkgs/testdata/test_tool_index.json @@ -514,61 +514,6 @@ "size": "6829396" } ] - }, - { - "name": "fwupdater", - "version": "0.1.12", - "systems": [ - { - "host": "i686-linux-gnu", - "url": "http://downloads.arduino.cc/tools/FirmwareUploader_0.1.12_Linux_32bit.tar.bz2", - "archiveFileName": "FirmwareUploader_0.1.12_Linux_32bit.tar.bz2", - "checksum": "SHA-256:2fec2bdfd20ad4950bc9ba37108dc2a7c152f569174279c0697efe1f5a0db781", - "size": "26097546" - }, - { - "host": "x86_64-pc-linux-gnu", - "url": "http://downloads.arduino.cc/tools/FirmwareUploader_0.1.12_Linux_64bit.tar.bz2", - "archiveFileName": "FirmwareUploader_0.1.12_Linux_64bit.tar.bz2", - "checksum": "SHA-256:ce57d0afef30cb7d3513f5da326346c99d6bf4923bbc2200634086811f3fb31e", - "size": "26073327" - }, - { - "host": "i686-mingw32", - "url": "http://downloads.arduino.cc/tools/FirmwareUploader_0.1.12_Windows_32bit.zip", - "archiveFileName": "FirmwareUploader_0.1.12_Windows_32bit.zip", - "checksum": "SHA-256:558568b453caa1c821def8cc6d34555d0c910eb7e7e871de3ae1c39ae6f01bdd", - "size": "25743641" - }, - { - "host": "x86_64-mingw32", - "url": "http://downloads.arduino.cc/tools/FirmwareUploader_0.1.12_Windows_64bit.zip", - "archiveFileName": "FirmwareUploader_0.1.12_Windows_64bit.zip", - "checksum": "SHA-256:ec16de33620985434280c92c3c322257b89bb67adf8fd4d5dd5f9467ea1e9e40", - "size": "25851428" - }, - { - "host": "i386-apple-darwin11", - "url": "http://downloads.arduino.cc/tools/FirmwareUploader_0.1.12_macOS_64bit.tar.bz2", - "archiveFileName": "FirmwareUploader_0.1.12_macOS_64bit.tar.bz2", - "checksum": "SHA-256:a470361b57f86ddfcaecd274d844af51ee1d23a71cd6c26e30fcef2152d1a03f", - "size": "25792860" - }, - { - "host": "arm-linux-gnueabihf", - "url": "http://downloads.arduino.cc/tools/FirmwareUploader_0.1.12_Linux_ARM.tar.bz2", - "archiveFileName": "FirmwareUploader_0.1.12_Linux_ARM.tar.bz2", - "checksum": "SHA-256:855fa0a9b942c3ee18906efc510bdfe30bf3334ff28ffbb476e648ff30033847", - "size": "25936245" - }, - { - "host": "aarch64-linux-gnu", - "url": "http://downloads.arduino.cc/tools/FirmwareUploader_0.1.12_Linux_ARM64.tar.bz2", - "archiveFileName": "FirmwareUploader_0.1.12_Linux_ARM64.tar.bz2", - "checksum": "SHA-256:691461e64fe075e9a79801347c2bd895fb72f8f2c45a7cd49056c6ad9efe8fc4", - "size": "25967430" - } - ] } ] } diff --git a/v2/pkgs/tools.go b/v2/pkgs/tools.go index 55ff6c2e4..7f34e3d08 100644 --- a/v2/pkgs/tools.go +++ b/v2/pkgs/tools.go @@ -18,6 +18,7 @@ package pkgs import ( "bytes" "context" + "crypto/rsa" "crypto/sha256" "encoding/hex" "encoding/json" @@ -29,11 +30,19 @@ import ( "path/filepath" "runtime" "strings" + "sync" "github.com/arduino/arduino-create-agent/gen/tools" "github.com/arduino/arduino-create-agent/index" "github.com/arduino/arduino-create-agent/utilities" - "github.com/codeclysm/extract/v3" + "github.com/blang/semver" + "github.com/codeclysm/extract/v4" +) + +// public vars to allow override in the tests +var ( + OS = runtime.GOOS + Arch = runtime.GOARCH ) // Tools is a client that implements github.com/arduino/arduino-create-agent/gen/tools.Service interface. @@ -50,18 +59,28 @@ import ( // // It requires an Index Resource to search for tools type Tools struct { - index *index.Resource - folder string + index *index.Resource + folder string + behaviour string + installed map[string]string + mutex sync.RWMutex + verifySignaturePubKey *rsa.PublicKey // public key used to verify the signature of a command sent to the boards } // New will return a Tool object, allowing the caller to execute operations on it. // The New function will accept an index as parameter (used to download the indexes) // and a folder used to download the indexes -func New(index *index.Resource, folder string) *Tools { - return &Tools{ - index: index, - folder: folder, +func New(index *index.Resource, folder, behaviour string, verifySignaturePubKey *rsa.PublicKey) *Tools { + t := &Tools{ + index: index, + folder: folder, + behaviour: behaviour, + installed: map[string]string{}, + mutex: sync.RWMutex{}, + verifySignaturePubKey: verifySignaturePubKey, } + t.readInstalled() + return t } // Installedhead is here only because it was required by the front-end. @@ -150,7 +169,7 @@ func (t *Tools) Install(ctx context.Context, payload *tools.ToolPayload) (*tools //if URL is defined and is signed we verify the signature and override the name, payload, version parameters if payload.URL != nil && payload.Signature != nil && payload.Checksum != nil { - err := utilities.VerifyInput(*payload.URL, *payload.Signature) + err := utilities.VerifyInput(*payload.URL, *payload.Signature, t.verifySignaturePubKey) if err != nil { return nil, err } @@ -166,21 +185,25 @@ func (t *Tools) Install(ctx context.Context, payload *tools.ToolPayload) (*tools var index Index json.Unmarshal(body, &index) - for _, packager := range index.Packages { - if packager.Name != payload.Packager { - continue - } + correctTool, correctSystem, found := FindTool(payload.Packager, payload.Name, payload.Version, index) + path = filepath.Join(payload.Packager, correctTool.Name, correctTool.Version) - for _, tool := range packager.Tools { - if tool.Name == payload.Name && - tool.Version == payload.Version { - - sys := tool.GetFlavourCompatibleWith(runtime.GOOS, runtime.GOARCH) - - return t.install(ctx, path, sys.URL, sys.Checksum) + key := correctTool.Name + "-" + correctTool.Version + // Check if it already exists + if t.behaviour == "keep" && pathExists(t.folder) { + location, ok := t.getInstalledValue(key) + if ok && pathExists(location) { + // overwrite the default tool with this one + err := t.writeInstalled(path) + if err != nil { + return nil, err } + return &tools.Operation{Status: "ok"}, nil } } + if found { + return t.install(ctx, path, correctSystem.URL, correctSystem.Checksum) + } return nil, tools.MakeNotFound( fmt.Errorf("tool not found with packager '%s', name '%s', version '%s'", @@ -229,7 +252,7 @@ func (t *Tools) install(ctx context.Context, path, url, checksum string) (*tools } // Write installed.json for retrocompatibility with v1 - err = writeInstalled(t.folder, path) + err = t.writeInstalled(path) if err != nil { return nil, err } @@ -253,45 +276,117 @@ func (t *Tools) Remove(ctx context.Context, payload *tools.ToolPayload) (*tools. return &tools.Operation{Status: "ok"}, nil } +// rename function is used to rename the path of the extracted files func rename(base string) extract.Renamer { + // "Rename" the given path adding the "base" and removing the root folder in "path" (if present). return func(path string) string { parts := strings.Split(filepath.ToSlash(path), "/") + if len(parts) <= 1 { + // The path does not contain a root folder. This might happen for tool packages (zip files) + // that have an invalid structure. Do not try to remove the root folder in these cases. + return filepath.Join(base, path) + } + // Removes the first part of the path (the root folder). path = strings.Join(parts[1:], "/") - path = filepath.Join(base, path) - return path + return filepath.Join(base, path) } } -func writeInstalled(folder, path string) error { +func (t *Tools) readInstalled() error { + t.mutex.RLock() + defer t.mutex.RUnlock() // read installed.json - installed := map[string]string{} - - installedFile, err := utilities.SafeJoin(folder, "installed.json") + installedFile, err := utilities.SafeJoin(t.folder, "installed.json") if err != nil { return err } data, err := os.ReadFile(installedFile) - if err == nil { - err = json.Unmarshal(data, &installed) - if err != nil { - return err - } + if err != nil { + return err } + return json.Unmarshal(data, &t.installed) +} + +func (t *Tools) writeInstalled(path string) error { + t.mutex.Lock() + defer t.mutex.Unlock() parts := strings.Split(path, string(filepath.Separator)) tool := parts[len(parts)-2] toolWithVersion := fmt.Sprint(tool, "-", parts[len(parts)-1]) - toolFile, err := utilities.SafeJoin(folder, path) + toolFile, err := utilities.SafeJoin(t.folder, path) + if err != nil { + return err + } + t.installed[tool] = toolFile + t.installed[toolWithVersion] = toolFile + + data, err := json.Marshal(t.installed) if err != nil { return err } - installed[tool] = toolFile - installed[toolWithVersion] = toolFile - data, err = json.Marshal(installed) + installedFile, err := utilities.SafeJoin(t.folder, "installed.json") if err != nil { return err } return os.WriteFile(installedFile, data, 0644) } + +// SetBehaviour sets the download behaviour to either keep or replace +func (t *Tools) SetBehaviour(behaviour string) { + t.behaviour = behaviour +} + +func (t *Tools) getInstalledValue(key string) (string, bool) { + t.mutex.RLock() + defer t.mutex.RUnlock() + location, ok := t.installed[key] + return location, ok +} + +func pathExists(path string) bool { + _, err := os.Stat(path) + if err == nil { + return true + } + if os.IsNotExist(err) { + return false + } + return true +} + +// FindTool searches the index for the correct tool and system that match the specified tool name and version +func FindTool(pack, name, version string, data Index) (Tool, System, bool) { + var correctTool Tool + correctTool.Version = "0.0" + found := false + + for _, p := range data.Packages { + if p.Name != pack { + continue + } + for _, t := range p.Tools { + if version != "latest" { + if t.Name == name && t.Version == version { + correctTool = t + found = true + } + } else { + // Find latest + v1, _ := semver.Make(t.Version) + v2, _ := semver.Make(correctTool.Version) + if t.Name == name && v1.Compare(v2) > 0 { + correctTool = t + found = true + } + } + } + } + + // Find the url based on system + correctSystem := correctTool.GetFlavourCompatibleWith(OS, Arch) + + return correctTool, correctSystem, found +} diff --git a/v2/pkgs/tools_test.go b/v2/pkgs/tools_test.go index 78c56398f..7bf0ff0e3 100644 --- a/v2/pkgs/tools_test.go +++ b/v2/pkgs/tools_test.go @@ -25,7 +25,9 @@ import ( "github.com/arduino/arduino-create-agent/config" "github.com/arduino/arduino-create-agent/gen/tools" + "github.com/arduino/arduino-create-agent/globals" "github.com/arduino/arduino-create-agent/index" + "github.com/arduino/arduino-create-agent/utilities" "github.com/arduino/arduino-create-agent/v2/pkgs" "github.com/arduino/go-paths-helper" "github.com/stretchr/testify/require" @@ -45,7 +47,7 @@ func TestTools(t *testing.T) { // Instantiate Index Index := index.Init(indexURL, config.GetDataDir()) - service := pkgs.New(Index, tmp) + service := pkgs.New(Index, tmp, "replace", utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))) ctx := context.Background() @@ -126,7 +128,7 @@ func TestEvilFilename(t *testing.T) { // Instantiate Index Index := index.Init(indexURL, config.GetDataDir()) - service := pkgs.New(Index, tmp) + service := pkgs.New(Index, tmp, "replace", utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))) ctx := context.Background() @@ -195,7 +197,7 @@ func TestInstalledHead(t *testing.T) { // Instantiate Index Index := index.Init(indexURL, config.GetDataDir()) - service := pkgs.New(Index, tmp) + service := pkgs.New(Index, tmp, "replace", utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))) ctx := context.Background() @@ -216,7 +218,7 @@ func TestInstall(t *testing.T) { LastRefresh: time.Now(), } - tool := pkgs.New(testIndex, tmp) + tool := pkgs.New(testIndex, tmp, "replace", utilities.MustParseRsaPublicKey([]byte(globals.ArduinoSignaturePubKey))) ctx := context.Background() @@ -230,9 +232,9 @@ func TestInstall(t *testing.T) { {Name: "dfu-util", Version: "0.10.0-arduino1", Packager: "arduino-test", URL: nil, Checksum: nil, Signature: nil}, {Name: "rp2040tools", Version: "1.0.6", Packager: "arduino-test", URL: nil, Checksum: nil, Signature: nil}, {Name: "esptool_py", Version: "4.5.1", Packager: "arduino-test", URL: nil, Checksum: nil, Signature: nil}, - // At the moment we don't install these ones because they are packaged in a different way: they do not have a top level dir, causing the rename funcion to behave incorrectly - // {Name: "fwupdater", Version: "0.1.12", Packager: "arduino-test", URL: nil, Checksum: nil, Signature: nil}, - // {Name: "arduino-fwuploader", Version: "2.2.2", Packager: "arduino-test", URL: nil, Checksum: nil, Signature: nil}, + {Name: "arduino-fwuploader", Version: "2.2.2", Packager: "arduino-test", URL: nil, Checksum: nil, Signature: nil}, + // test download of a tool not present in index. the same archive is downloaded on linux/win/mac See https://github.com/arduino/arduino-create-agent/issues/980 + {Name: "esptool", Version: "2.5.0-3-20ed2b9", Packager: "esp8266", URL: strpoint("https://github.com/earlephilhower/esp-quick-toolchain/releases/download/2.5.0-3/x86_64-linux-gnu.esptool-f80ae31.tar.gz"), Checksum: strpoint("SHA-256:bded1dca953377838b6086a9bcd40a1dc5286ba5f69f2372c22a1d1819baad24"), Signature: strpoint("852b58871419ce5e5633ecfaa72c0f0fa890ceb51164b362b8133bc0e3e003a21cec48935b8cdc078f4031219cbf17fb7edd9d7c9ca8ed85492911c9ca6353c9aa4691eb91fda99563a6bd49aeca0d9981fb05ec76e45c6024f8a6822862ad1e34ddc652fbbf4fa909887a255d4f087398ec386577efcec523c21203be3d10fc9e9b0f990a7536875a77dc2bc5cbffea7734b62238e31719111b718bacccebffc9be689545540e81d23b81caa66214376f58a0d6a45cf7efc5d3af62ab932b371628162fffe403906f41d5534921e5be081c5ac2ecc9db5caec03a105cc44b00ce19a95ad079843501eb8182e0717ce327867380c0e39d2b48698547fc1d0d66")}, } expectedFiles := map[string][]string{ @@ -244,8 +246,8 @@ func TestInstall(t *testing.T) { "dfu-util-0.10.0-arduino1": {"dfu-prefix", "dfu-suffix", "dfu-util"}, "rp2040tools-1.0.6": {"elf2uf2", "picotool", "pioasm", "rp2040load"}, "esptool_py-4.5.1": {"esptool"}, - // "fwupdater-0.1.12": {"firmwares", "FirmwareUploader"}, // old legacy tool - // "arduino-fwuploader-2.2.2": {"arduino-fwuploader"}, + "arduino-fwuploader-2.2.2": {"arduino-fwuploader"}, + // "esptool-2.5.0-3-20ed2b9": {"esptool"}, // we don't check if there is esptool in the archive because it's the same archive even on windows (no extension) } for _, tc := range testCases { t.Run(tc.Name+"-"+tc.Version, func(t *testing.T) { @@ -254,7 +256,7 @@ func TestInstall(t *testing.T) { require.NoError(t, err) // Check that the tool has been downloaded - toolDir := paths.New(tmp).Join("arduino-test", tc.Name, tc.Version) + toolDir := paths.New(tmp).Join(tc.Packager, tc.Name, tc.Version) require.DirExists(t, toolDir.String()) // Check that the files have been created