diff --git a/.github/workflows/release-go-task.yml b/.github/workflows/release-go-task.yml index db0d696f..b19e2b8d 100644 --- a/.github/workflows/release-go-task.yml +++ b/.github/workflows/release-go-task.yml @@ -9,8 +9,6 @@ env: # The project's folder on Arduino's download server for uploading builds AWS_PLUGIN_TARGET: TODO ARTIFACT_NAME: dist - # TODO: Remember to REMOVE binaries folder as soon as it is removed from the project - PROVISIONING_BINARIES_FOLDER: binaries on: push: @@ -104,7 +102,6 @@ jobs: # This step performs the following: # 1. Repackage the signed binary replaced in place by Gon (ignoring the output zip file) # 2. Recalculate package checksum and replace it in the nnnnnn-checksums.txt file - # TODO: Remember to REMOVE binaries folder as soon as it is removed from the project ({{.PROVISIONING_BINARIES_FOLDER}}) run: | # GitHub's upload/download-artifact@v2 actions don't preserve file permissions, # so we need to add execution permission back until the action is made to do this. @@ -112,7 +109,7 @@ jobs: TAG="${GITHUB_REF/refs\/tags\//}" tar -czvf "${{ env.DIST_DIR }}/${{ env.PROJECT_NAME }}_${TAG}_macOS_64bit.tar.gz" \ -C ${{ env.DIST_DIR }}/${{ env.PROJECT_NAME }}_osx_darwin_amd64/ ${{ env.PROJECT_NAME }} \ - -C ../../ ${{ env.PROVISIONING_BINARIES_FOLDER }} LICENSE.txt + -C ../../ LICENSE.txt CHECKSUM="$(shasum -a 256 ${{ env.DIST_DIR }}/${{ env.PROJECT_NAME }}_${TAG}_macOS_64bit.tar.gz | cut -d " " -f 1)" perl \ -pi \ diff --git a/DistTasks.yml b/DistTasks.yml index 0151ded1..70b95b68 100644 --- a/DistTasks.yml +++ b/DistTasks.yml @@ -39,7 +39,6 @@ tasks: Windows_32bit: desc: Builds Windows 32 bit binaries dir: "{{.DIST_DIR}}" - # TODO: Remember to REMOVE binaries folder as soon as it is removed from the project cmds: - | docker run -v `pwd`/..:/home/build -w /home/build \ @@ -50,7 +49,7 @@ tasks: cp {{.PLATFORM_DIR}}/{{.PROJECT_NAME}}.exe ../ cd .. - zip -r {{.DIST_DIR}}/{{.PACKAGE_NAME}} {{.PROJECT_NAME}}.exe {{.PROVISIONING_BINARIES_FOLDER}} LICENSE.txt + zip -r {{.DIST_DIR}}/{{.PACKAGE_NAME}} {{.PROJECT_NAME}}.exe LICENSE.txt cd {{.DIST_DIR}} sha256sum {{.PACKAGE_NAME}} >> {{.CHECKSUM_FILE}} @@ -75,7 +74,7 @@ tasks: cp {{.PLATFORM_DIR}}/{{.PROJECT_NAME}}.exe ../ cd .. - zip -r {{.DIST_DIR}}/{{.PACKAGE_NAME}} {{.PROJECT_NAME}}.exe {{.PROVISIONING_BINARIES_FOLDER}} LICENSE.txt + zip -r {{.DIST_DIR}}/{{.PACKAGE_NAME}} {{.PROJECT_NAME}}.exe LICENSE.txt cd {{.DIST_DIR}} sha256sum {{.PACKAGE_NAME}} >> {{.CHECKSUM_FILE}} @@ -98,7 +97,7 @@ tasks: --build-cmd "{{.BUILD_COMMAND}}" \ -p "{{.BUILD_PLATFORM}}" - tar cz -C {{.PLATFORM_DIR}} {{.PROJECT_NAME}} -C ../.. {{.PROVISIONING_BINARIES_FOLDER}} LICENSE.txt -f {{.PACKAGE_NAME}} + tar cz -C {{.PLATFORM_DIR}} {{.PROJECT_NAME}} -C ../.. LICENSE.txt -f {{.PACKAGE_NAME}} sha256sum {{.PACKAGE_NAME}} >> {{.CHECKSUM_FILE}} vars: @@ -120,7 +119,7 @@ tasks: --build-cmd "{{.BUILD_COMMAND}}" \ -p "{{.BUILD_PLATFORM}}" - tar cz -C {{.PLATFORM_DIR}} {{.PROJECT_NAME}} -C ../.. {{.PROVISIONING_BINARIES_FOLDER}} LICENSE.txt -f {{.PACKAGE_NAME}} + tar cz -C {{.PLATFORM_DIR}} {{.PROJECT_NAME}} -C ../.. LICENSE.txt -f {{.PACKAGE_NAME}} sha256sum {{.PACKAGE_NAME}} >> {{.CHECKSUM_FILE}} vars: @@ -142,7 +141,7 @@ tasks: --build-cmd "{{.BUILD_COMMAND}}" \ -p "{{.BUILD_PLATFORM}}" - tar cz -C {{.PLATFORM_DIR}} {{.PROJECT_NAME}} -C ../.. {{.PROVISIONING_BINARIES_FOLDER}} LICENSE.txt -f {{.PACKAGE_NAME}} + tar cz -C {{.PLATFORM_DIR}} {{.PROJECT_NAME}} -C ../.. LICENSE.txt -f {{.PACKAGE_NAME}} sha256sum {{.PACKAGE_NAME}} >> {{.CHECKSUM_FILE}} vars: @@ -164,7 +163,7 @@ tasks: --build-cmd "{{.BUILD_COMMAND}}" \ -p "{{.BUILD_PLATFORM}}" - tar cz -C {{.PLATFORM_DIR}} {{.PROJECT_NAME}} -C ../.. {{.PROVISIONING_BINARIES_FOLDER}} LICENSE.txt -f {{.PACKAGE_NAME}} + tar cz -C {{.PLATFORM_DIR}} {{.PROJECT_NAME}} -C ../.. LICENSE.txt -f {{.PACKAGE_NAME}} sha256sum {{.PACKAGE_NAME}} >> {{.CHECKSUM_FILE}} vars: @@ -214,7 +213,7 @@ tasks: --build-cmd "{{.BUILD_COMMAND}}" \ -p "{{.BUILD_PLATFORM}}" - tar cz -C {{.PLATFORM_DIR}} {{.PROJECT_NAME}} -C ../.. {{.PROVISIONING_BINARIES_FOLDER}} LICENSE.txt -f {{.PACKAGE_NAME}} + tar cz -C {{.PLATFORM_DIR}} {{.PROJECT_NAME}} -C ../.. LICENSE.txt -f {{.PACKAGE_NAME}} sha256sum {{.PACKAGE_NAME}} >> {{.CHECKSUM_FILE}} vars: @@ -236,7 +235,7 @@ tasks: --build-cmd "{{.BUILD_COMMAND}}" \ -p "{{.BUILD_PLATFORM}}" - tar cz -C {{.PLATFORM_DIR}} {{.PROJECT_NAME}} -C ../.. {{.PROVISIONING_BINARIES_FOLDER}} LICENSE.txt -f {{.PACKAGE_NAME}} + tar cz -C {{.PLATFORM_DIR}} {{.PROJECT_NAME}} -C ../.. LICENSE.txt -f {{.PACKAGE_NAME}} sha256sum {{.PACKAGE_NAME}} >> {{.CHECKSUM_FILE}} vars: diff --git a/README.md b/README.md index 8acd193a..4a040587 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,8 @@ This code is licensed under the terms of the GNU Affero General Public License v ### Requirements -This is all you need to use arduino-cloud-cli for device **provisioning**: +This is all you need to use arduino-cloud-cli: * A client ID and a secret ID, retrievable from the [cloud](https://create.arduino.cc/iot/integrations) by creating a new API key - * The folder containing the precompiled provisioning firmwares (`binaries`) needs to be in the same location you run the command from ### Additional info diff --git a/binaries/arduino.mbed_nano.nanorp2040connect.elf b/binaries/arduino.mbed_nano.nanorp2040connect.elf deleted file mode 100755 index 5a445b78..00000000 Binary files a/binaries/arduino.mbed_nano.nanorp2040connect.elf and /dev/null differ diff --git a/binaries/arduino.mbed_portenta.envie_m7.bin b/binaries/arduino.mbed_portenta.envie_m7.bin deleted file mode 100755 index 17663d4b..00000000 Binary files a/binaries/arduino.mbed_portenta.envie_m7.bin and /dev/null differ diff --git a/binaries/arduino.samd.mkr1000.bin b/binaries/arduino.samd.mkr1000.bin deleted file mode 100755 index 9eb8d6ed..00000000 Binary files a/binaries/arduino.samd.mkr1000.bin and /dev/null differ diff --git a/binaries/arduino.samd.mkrgsm1400.bin b/binaries/arduino.samd.mkrgsm1400.bin deleted file mode 100755 index c7f8c6e9..00000000 Binary files a/binaries/arduino.samd.mkrgsm1400.bin and /dev/null differ diff --git a/binaries/arduino.samd.mkrnb1500.bin b/binaries/arduino.samd.mkrnb1500.bin deleted file mode 100755 index 95df1f4d..00000000 Binary files a/binaries/arduino.samd.mkrnb1500.bin and /dev/null differ diff --git a/binaries/arduino.samd.mkrwifi1010.bin b/binaries/arduino.samd.mkrwifi1010.bin deleted file mode 100755 index 3c3cb6b5..00000000 Binary files a/binaries/arduino.samd.mkrwifi1010.bin and /dev/null differ diff --git a/binaries/arduino.samd.nano_33_iot.bin b/binaries/arduino.samd.nano_33_iot.bin deleted file mode 100755 index 50c2881c..00000000 Binary files a/binaries/arduino.samd.nano_33_iot.bin and /dev/null differ diff --git a/binaries/getdeveui.arduino.samd.mkrwan1300.bin b/binaries/getdeveui.arduino.samd.mkrwan1300.bin deleted file mode 100755 index f8864ad0..00000000 Binary files a/binaries/getdeveui.arduino.samd.mkrwan1300.bin and /dev/null differ diff --git a/binaries/getdeveui.arduino.samd.mkrwan1310.bin b/binaries/getdeveui.arduino.samd.mkrwan1310.bin deleted file mode 100755 index 7e31365b..00000000 Binary files a/binaries/getdeveui.arduino.samd.mkrwan1310.bin and /dev/null differ diff --git a/command/device/board.go b/command/device/board.go new file mode 100644 index 00000000..f0eaf527 --- /dev/null +++ b/command/device/board.go @@ -0,0 +1,124 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package device + +import ( + "strings" + + rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1" +) + +var ( + cryptoFQBN = []string{ + "arduino:samd:nano_33_iot", + "arduino:samd:mkrwifi1010", + "arduino:mbed_nano:nanorp2040connect", + "arduino:mbed_portenta:envie_m7", + "arduino:samd:mkr1000", + "arduino:samd:mkrgsm1400", + "arduino:samd:mkrnb1500", + } + loraFQBN = []string{ + "arduino:samd:mkrwan1310", + "arduino:samd:mkrwan1300", + } +) + +// board contains details of a physical arduino board +type board struct { + fqbn string + serial string + dType string + port string +} + +// isCrypto checks if the board is a valid arduino board with a +// supported crypto-chip +func (b *board) isCrypto() bool { + for _, f := range cryptoFQBN { + if b.fqbn == f { + return true + } + } + return false +} + +// isCrypto checks if the board is a valid LoRa arduino board +func (b *board) isLora() bool { + for _, f := range loraFQBN { + if b.fqbn == f { + return true + } + } + return false +} + +// boardFromPorts returns a board that matches all the criteria +// passed in. If no criteria are passed, it returns the first board found. +func boardFromPorts(ports []*rpc.DetectedPort, params *CreateParams) *board { + for _, port := range ports { + if portFilter(port, params) { + continue + } + boardFound := boardFilter(port.Boards, params) + if boardFound != nil { + b := &board{ + fqbn: boardFound.Fqbn, + serial: port.SerialNumber, + dType: strings.Split(boardFound.Fqbn, ":")[2], + port: port.Address, + } + return b + } + } + + return nil +} + +// portFilter filters out the given port in the following cases: +// - if the port parameter does not match the actual port address. +// - if the the detected port does not contain any board. +// It returns: +// true -> to skip the port +// false -> to keep the port +func portFilter(port *rpc.DetectedPort, params *CreateParams) bool { + if len(port.Boards) == 0 { + return true + } + if params.Port != nil && *params.Port != port.Address { + return true + } + return false +} + +// boardFilter looks for a board which has the same fqbn passed as parameter. +// If fqbn parameter is nil, then the first board found is returned. +// It returns: +// - a board if it is found. +// - nil if no board matching the fqbn parameter is found. +func boardFilter(boards []*rpc.BoardListItem, params *CreateParams) (board *rpc.BoardListItem) { + if params.Fqbn == nil { + return boards[0] + } + for _, b := range boards { + if b.Fqbn == *params.Fqbn { + return b + } + } + return +} diff --git a/command/device/create_test.go b/command/device/board_test.go similarity index 100% rename from command/device/create_test.go rename to command/device/board_test.go diff --git a/command/device/create.go b/command/device/create.go index c0c08f78..6042d93b 100644 --- a/command/device/create.go +++ b/command/device/create.go @@ -20,9 +20,7 @@ package device import ( "errors" "fmt" - "strings" - rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1" "github.com/arduino/arduino-cloud-cli/arduino/cli" "github.com/arduino/arduino-cloud-cli/internal/config" "github.com/arduino/arduino-cloud-cli/internal/iot" @@ -37,13 +35,6 @@ type CreateParams struct { Fqbn *string // Board FQBN - Optional - If omitted then the first device found gets selected } -type board struct { - fqbn string - serial string - dType string - port string -} - // Create command is used to provision a new arduino device // and to add it to Arduino IoT Cloud. func Create(params *CreateParams) (*DeviceInfo, error) { @@ -62,6 +53,16 @@ func Create(params *CreateParams) (*DeviceInfo, error) { return nil, err } + if !board.isCrypto() { + return nil, fmt.Errorf( + "board with fqbn %s found at port %s is not a device with a supported crypto-chip.\n"+ + "Try the 'create-lora' command instead if it's a LoRa device"+ + " or 'create-generic' otherwise", + board.fqbn, + board.port, + ) + } + conf, err := config.Retrieve() if err != nil { return nil, err @@ -104,54 +105,3 @@ func Create(params *CreateParams) (*DeviceInfo, error) { } return devInfo, nil } - -// boardFromPorts returns a board that matches all the criteria -// passed in. If no criteria are passed, it returns the first board found. -func boardFromPorts(ports []*rpc.DetectedPort, params *CreateParams) *board { - for _, port := range ports { - if portFilter(port, params) { - continue - } - boardFound := boardFilter(port.Boards, params) - if boardFound != nil { - t := strings.Split(boardFound.Fqbn, ":")[2] - b := &board{boardFound.Fqbn, port.SerialNumber, t, port.Address} - return b - } - } - - return nil -} - -// portFilter filters out the given port in the following cases: -// - if the port parameter does not match the actual port address. -// - if the the detected port does not contain any board. -// It returns: -// true -> to skip the port -// false -> to keep the port -func portFilter(port *rpc.DetectedPort, params *CreateParams) bool { - if len(port.Boards) == 0 { - return true - } - if params.Port != nil && *params.Port != port.Address { - return true - } - return false -} - -// boardFilter looks for a board which has the same fqbn passed as parameter. -// If fqbn parameter is nil, then the first board found is returned. -// It returns: -// - a board if it is found. -// - nil if no board matching the fqbn parameter is found. -func boardFilter(boards []*rpc.BoardListItem, params *CreateParams) (board *rpc.BoardListItem) { - if params.Fqbn == nil { - return boards[0] - } - for _, b := range boards { - if b.Fqbn == *params.Fqbn { - return b - } - } - return -} diff --git a/command/device/createlora.go b/command/device/createlora.go index 4b8501b8..58e34590 100644 --- a/command/device/createlora.go +++ b/command/device/createlora.go @@ -1,11 +1,25 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + package device import ( "errors" "fmt" - "os" - "path/filepath" - "strings" "time" "github.com/arduino/arduino-cloud-cli/arduino/cli" @@ -64,9 +78,19 @@ func CreateLora(params *CreateLoraParams) (*DeviceLoraInfo, error) { return nil, err } - bin, err := deveuiBinary(board.fqbn) + if !board.isLora() { + return nil, fmt.Errorf( + "board with fqbn %s found at port %s is not a LoRa device."+ + " Try the 'create' command instead if it's a device with a supported crypto-chip"+ + " or 'create-generic' otherwise", + board.fqbn, + board.port, + ) + } + + bin, err := downloadProvisioningFile(board.fqbn) if err != nil { - return nil, fmt.Errorf("fqbn not supported for LoRa provisioning: %w", err) + return nil, err } logrus.Infof("%s", "Uploading deveui sketch on the LoRa board") @@ -114,22 +138,6 @@ func CreateLora(params *CreateLoraParams) (*DeviceLoraInfo, error) { return devInfo, nil } -// deveuiBinary gets the absolute path of the deveui binary corresponding to the -// provisioned board's fqbn. It is contained in the local binaries folder. -func deveuiBinary(fqbn string) (string, error) { - // Use local binaries until they are uploaded online - bin := filepath.Join("./binaries/", "getdeveui."+strings.ReplaceAll(fqbn, ":", ".")+".bin") - bin, err := filepath.Abs(bin) - if err != nil { - return "", fmt.Errorf("getting the deveui binary: %w", err) - } - if _, err := os.Stat(bin); os.IsNotExist(err) { - err = fmt.Errorf("%s: %w", "deveui binary not found", err) - return "", err - } - return bin, nil -} - // extractEUI extracts the EUI from the provisioned lora board. func extractEUI(port string) (string, error) { var ser serial.Port diff --git a/command/device/provision.go b/command/device/provision.go index 9baa8cbb..0cba536f 100644 --- a/command/device/provision.go +++ b/command/device/provision.go @@ -20,18 +20,51 @@ package device import ( "encoding/hex" "fmt" - "os" "path/filepath" "strconv" - "strings" "time" "github.com/arduino/arduino-cloud-cli/arduino" + "github.com/arduino/arduino-cloud-cli/internal/binary" "github.com/arduino/arduino-cloud-cli/internal/iot" "github.com/arduino/arduino-cloud-cli/internal/serial" + "github.com/arduino/go-paths-helper" "github.com/sirupsen/logrus" ) +// downloadProvisioningFile downloads and returns the absolute path +// of the provisioning binary corresponding to the passed fqbn. +func downloadProvisioningFile(fqbn string) (string, error) { + index, err := binary.LoadIndex() + if err != nil { + return "", err + } + bin := index.FindProvisionBin(fqbn) + if bin == nil { + return "", fmt.Errorf("provisioning binary for board %s not found", fqbn) + } + bytes, err := binary.Download(bin) + if err != nil { + return "", fmt.Errorf("downloading provisioning binary: %w", err) + } + + // Save provision binary always in the same temporary folder to + // avoid wasting user's storage. + filename := filepath.Base(bin.URL) + path := paths.TempDir().Join("cloud-cli").Join(filename) + path.Parent().MkdirAll() + if err = path.WriteFile(bytes); err != nil { + return "", fmt.Errorf("writing provisioning binary: %w", err) + } + p, err := path.Abs() + if err != nil { + return "", fmt.Errorf("cannot retrieve absolute path of downloaded binary: %w", err) + } + return p.String(), nil +} + +// provision is responsible for running the provisioning +// procedures for boards with crypto-chip type provision struct { arduino.Commander iot.Client @@ -40,14 +73,7 @@ type provision struct { id string } -type binFile struct { - Bin string `json:"bin"` - Filename string `json:"filename"` - Fqbn string `json:"fqbn"` - Name string `json:"name"` - Sha256 string `json:"sha256"` -} - +// run provisioning procedure for boards with crypto-chip func (p provision) run() error { bin, err := downloadProvisioningFile(p.board.fqbn) if err != nil { @@ -195,79 +221,6 @@ func (p provision) configBoard() error { return nil } -func downloadProvisioningFile(fqbn string) (string, error) { - // Use local binaries until they are uploaded online - bin := filepath.Join("./binaries/", strings.ReplaceAll(fqbn, ":", ".")+".bin") - bin, err := filepath.Abs(bin) - if err != nil { - return "", err - } - if _, err := os.Stat(bin); err == nil { - return bin, nil - } - - elf := filepath.Join("./binaries/", strings.ReplaceAll(fqbn, ":", ".")+".elf") - elf, err = filepath.Abs(elf) - if err != nil { - return "", err - } - if _, err := os.Stat(elf); os.IsNotExist(err) { - err = fmt.Errorf("%s: %w", "fqbn not supported", err) - return "", err - } - return elf, nil - - // TODO: upload binaries on some arduino page and enable this flow - //url := "https://api2.arduino.cc/iot/v2/binaries/provisioning?fqbn=" + fqbn - //path, _ := filepath.Abs("./provisioning.bin") - - //cl := http.Client{ - //Timeout: time.Second * 3, // Timeout after 2 seconds - //} - - //req, err := http.NewRequest(http.MethodGet, url, nil) - //if err != nil { - //err = fmt.Errorf("%s: %w", "request provisioning binary", err) - //return "", err - //} - //res, err := cl.Do(req) - //if err != nil { - //err = fmt.Errorf("%s: %w", "request provisioning binary", err) - //return "", err - //} - - //if res.Body != nil { - //defer res.Body.Close() - //} - - //body, err := ioutil.ReadAll(res.Body) - //if err != nil { - //err = fmt.Errorf("%s: %w", "read provisioning request body", err) - //return "", err - //} - - //bin := binFile{} - //err = json.Unmarshal(body, &bin) - //if err != nil { - //err = fmt.Errorf("%s: %w", "unmarshal provisioning binary", err) - //return "", err - //} - - //bytes, err := base64.StdEncoding.DecodeString(bin.Bin) - //if err != nil { - //err = fmt.Errorf("%s: %w", "decoding provisioning binary", err) - //return "", err - //} - - //err = ioutil.WriteFile(path, bytes, 0666) - //if err != nil { - //err = fmt.Errorf("%s: %w", "downloading provisioning binary", err) - //return "", err - //} - - //return path, nil -} - func retry(tries int, sleep time.Duration, errMsg string, fun func() error) error { var err error for n := 0; n < tries; n++ { diff --git a/go.mod b/go.mod index 3ba2bd41..bea5d751 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/antihax/optional v1.0.0 github.com/arduino/arduino-cli v0.0.0-20210607095659-16f41352eac3 + github.com/arduino/arduino-fwuploader v0.0.0-20211202112845-b7f323ad97e2 github.com/arduino/go-paths-helper v1.6.1 github.com/arduino/go-win32-utils v0.0.0-20180330194947-ed041402e83b github.com/arduino/iot-client-go v1.3.4-0.20211116175324-9a98dd4ad269 @@ -20,6 +21,7 @@ require ( github.com/spf13/viper v1.7.0 github.com/stretchr/testify v1.6.1 go.bug.st/serial v1.3.0 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 golang.org/x/sys v0.0.0-20211209171907-798191bca915 // indirect diff --git a/go.sum b/go.sum index 327434ab..6745841e 100644 --- a/go.sum +++ b/go.sum @@ -46,12 +46,16 @@ github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/arduino/arduino-cli v0.0.0-20210603144340-aef5a54882fa/go.mod h1:HNbHWr7qq+9M2rhzBUJIBIpCMRlB6+mptNDLMDZNlG0= github.com/arduino/arduino-cli v0.0.0-20210607095659-16f41352eac3 h1:xiVTIUhB5Ewe2a9KYFc8BQDjDy8wXZ34kNiov7g0Ghs= github.com/arduino/arduino-cli v0.0.0-20210607095659-16f41352eac3/go.mod h1:72+QkT2EYMLmAtoDk2SKlndl+KT/O9/wKCqUtsrIjTg= +github.com/arduino/arduino-fwuploader v0.0.0-20211202112845-b7f323ad97e2 h1:ADkeoCxUZ5/uTy7d7e3XXsm11pXwVg2lvFOxmjDfKls= +github.com/arduino/arduino-fwuploader v0.0.0-20211202112845-b7f323ad97e2/go.mod h1:t6n09kqbFg+AxuzXJFUZaC3i8VPczEZi2SyY7axs+Rs= github.com/arduino/board-discovery v0.0.0-20180823133458-1ba29327fb0c h1:agh2JT96G8egU7FEb13L4dq3fnCN7lxXhJ86t69+W7s= github.com/arduino/board-discovery v0.0.0-20180823133458-1ba29327fb0c/go.mod h1:HK7SpkEax/3P+0w78iRQx1sz1vCDYYw9RXwHjQTB5i8= github.com/arduino/go-paths-helper v1.0.1/go.mod h1:HpxtKph+g238EJHq4geEPv9p+gl3v5YYu35Yb+w31Ck= github.com/arduino/go-paths-helper v1.2.0/go.mod h1:HpxtKph+g238EJHq4geEPv9p+gl3v5YYu35Yb+w31Ck= +github.com/arduino/go-paths-helper v1.5.0/go.mod h1:V82BWgAAp4IbmlybxQdk9Bpkz8M4Qyx+RAFKaG9NuvU= github.com/arduino/go-paths-helper v1.6.0/go.mod h1:V82BWgAAp4IbmlybxQdk9Bpkz8M4Qyx+RAFKaG9NuvU= github.com/arduino/go-paths-helper v1.6.1 h1:lha+/BuuBsx0qTZ3gy6IO1kU23lObWdQ/UItkzVWQ+0= github.com/arduino/go-paths-helper v1.6.1/go.mod h1:V82BWgAAp4IbmlybxQdk9Bpkz8M4Qyx+RAFKaG9NuvU= @@ -120,7 +124,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -279,13 +282,12 @@ github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYt github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/marcinbor85/gohex v0.0.0-20210308104911-55fb1c624d84/go.mod h1:Pb6XcsXyropB9LNHhnqaknG/vEwYztLkQzVCHv8sQ3M= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= @@ -572,8 +574,6 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6 h1:cdsMqa2nXzqlgs183pHxtvoVwU7CyzaCTAUOg94af4c= -golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211209171907-798191bca915 h1:P+8mCzuEpyszAT6T42q0sxU+eveBAF/cJ2Kp0x6/8+0= diff --git a/internal/binary/download.go b/internal/binary/download.go new file mode 100644 index 00000000..714d092e --- /dev/null +++ b/internal/binary/download.go @@ -0,0 +1,80 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package binary + +import ( + "bytes" + "errors" + "fmt" + "io/ioutil" + "net/http" + "time" + + fwuploader "github.com/arduino/arduino-fwuploader/indexes/download" +) + +// Download a binary file contained in the binary index +func Download(bin *IndexBin) ([]byte, error) { + b, err := download(bin.URL) + if err != nil { + return nil, fmt.Errorf("cannot download binary at %s: %w", bin.URL, err) + } + + sz, err := bin.Size.Int64() + if err != nil { + return nil, fmt.Errorf("cannot retrieve binary size: %w", err) + } + if len(b) != int(sz) { + return nil, fmt.Errorf("download failed: invalid binary size, expected %d bytes but got %d", sz, len(b)) + } + + err = fwuploader.VerifyChecksum(bin.Checksum, bytes.NewReader(b)) + if err != nil { + return nil, fmt.Errorf("verifying binary checksum: %w", err) + } + + return b, nil +} + +func download(url string) ([]byte, error) { + cl := http.Client{ + Timeout: time.Second * 3, + } + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + err = fmt.Errorf("%s: %w", "request url", err) + return nil, err + } + res, err := cl.Do(req) + if err != nil { + err = fmt.Errorf("%s: %w", "do request url", err) + return nil, err + } + + if res.Body == nil { + return nil, errors.New("empty file downloaded") + } + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + err = fmt.Errorf("%s: %w", "read request body", err) + return nil, err + } + return body, nil +} diff --git a/internal/binary/gpgkey/index_public.gpg.key b/internal/binary/gpgkey/index_public.gpg.key new file mode 100644 index 00000000..5374b109 Binary files /dev/null and b/internal/binary/gpgkey/index_public.gpg.key differ diff --git a/internal/binary/gpgkey/key.go b/internal/binary/gpgkey/key.go new file mode 100644 index 00000000..e783d26a --- /dev/null +++ b/internal/binary/gpgkey/key.go @@ -0,0 +1,27 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package gpgkey + +import ( + _ "embed" +) + +var ( + //go:embed index_public.gpg.key + IndexPublicKey []byte +) diff --git a/internal/binary/index.go b/internal/binary/index.go new file mode 100644 index 00000000..ef67b795 --- /dev/null +++ b/internal/binary/index.go @@ -0,0 +1,106 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package binary + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + + "compress/gzip" + + "github.com/arduino/arduino-cloud-cli/internal/binary/gpgkey" + "golang.org/x/crypto/openpgp" +) + +const ( + // URL of cloud-team binary index + IndexGZURL = "https://cloud-downloads.arduino.cc/binaries/index.json.gz" + IndexSigURL = "https://cloud-downloads.arduino.cc/binaries/index.json.sig" +) + +// Index contains details about all the binaries +// loaded in 'cloud-downloads' +type Index struct { + Boards []IndexBoard `json:"boards"` +} + +// IndexBoard describes all the binaries available for a specific board +type IndexBoard struct { + Fqbn string `json:"fqbn"` + Provision *IndexBin `json:"provision"` +} + +// IndexBin contains the details needed to retrieve a binary file from the cloud +type IndexBin struct { + URL string `json:"url"` + Checksum string `json:"checksum"` + Size json.Number `json:"size"` +} + +// LoadIndex downloads and verifies the index of binaries +// contained in 'cloud-downloads'. +func LoadIndex() (*Index, error) { + indexGZ, err := download(IndexGZURL) + if err != nil { + return nil, fmt.Errorf("cannot download index: %w", err) + } + + indexReader, err := gzip.NewReader(bytes.NewReader(indexGZ)) + if err != nil { + return nil, fmt.Errorf("cannot decompress index: %w", err) + } + index, err := ioutil.ReadAll(indexReader) + if err != nil { + return nil, fmt.Errorf("cannot read downloaded index: %w", err) + } + + sig, err := download(IndexSigURL) + if err != nil { + return nil, fmt.Errorf("cannot download index signature: %w", err) + } + + keyRing, err := openpgp.ReadKeyRing(bytes.NewReader(gpgkey.IndexPublicKey)) + if err != nil { + return nil, fmt.Errorf("cannot retrieve Arduino public GPG key: %w", err) + } + + signer, err := openpgp.CheckDetachedSignature(keyRing, bytes.NewReader(index), bytes.NewReader(sig)) + if signer == nil || err != nil { + return nil, fmt.Errorf("invalid signature for index downloaded from %s", IndexGZURL) + } + + i := &Index{} + if err = json.Unmarshal(index, &i); err != nil { + return nil, fmt.Errorf("cannot unmarshal index json: %w", err) + } + return i, nil +} + +// FindProvisionBin looks for the provisioning binary corresponding +// to the passed fqbn in the index. +// Returns nil if the binary is not found +func (i *Index) FindProvisionBin(fqbn string) *IndexBin { + for _, b := range i.Boards { + if b.Fqbn == fqbn { + return b.Provision + } + } + return nil +} diff --git a/internal/binary/index_test.go b/internal/binary/index_test.go new file mode 100644 index 00000000..b657263b --- /dev/null +++ b/internal/binary/index_test.go @@ -0,0 +1,46 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package binary + +import ( + "testing" +) + +func TestFindProvisionBin(t *testing.T) { + var ( + fqbnOK1 = "arduino:samd:nano_33_iot" + fqbnOK2 = "arduino:samd:mkrwifi1010" + fqbnNotFound = "arduino:mbed_nano:nano33ble" + ) + index := &Index{ + Boards: []IndexBoard{ + {Fqbn: fqbnOK1, Provision: &IndexBin{URL: "mkr"}}, + {Fqbn: fqbnOK2, Provision: &IndexBin{URL: "nano"}}, + }, + } + + bin := index.FindProvisionBin(fqbnOK2) + if bin == nil { + t.Fatal("provision binary not found") + } + + bin = index.FindProvisionBin(fqbnNotFound) + if bin != nil { + t.Fatalf("provision binary should've not be found, but got: %v", bin) + } +}