diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 98e103a..f6bc93e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,8 +7,20 @@ updates: directory: ".github/workflows" schedule: interval: "monthly" + day: "monday" + timezone: "Europe/Paris" + groups: + gh-dependency: + patterns: + - "*" - package-ecosystem: "gomod" directory: "/" schedule: interval: "monthly" + day: "monday" + timezone: "Europe/Paris" + groups: + go-dependency: + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38431d5..1d9af94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,10 +6,10 @@ name: Continuous Integration on: pull_request: branches: - - master + - main push: branches: - - master + - main jobs: lint-and-build: @@ -18,18 +18,19 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.22.x + go-version: 1.25.x cache-dependency-path: "go.sum" - name: Check linting - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v8 with: - version: v1.56.2 + version: v2.4.0 + args: --config ./.golangci.yml - name: Build Binaries run: | diff --git a/.golangci.yml b/.golangci.yml index c6c8f3b..66db170 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,75 +1,94 @@ # Copyright (c) Abstract Machines # SPDX-License-Identifier: Apache-2.0 +version: "2" run: concurrency: 4 - timeout: 1m - -linters-settings: - importas: - no-unaliased: true - no-extra-aliases: false - gocritic: - enabled-checks: - - importShadow - - httpNoBody - - paramTypeCombine - - emptyStringTest - - builtinShadow - - exposedSyncMutex - disabled-checks: - - appendAssign - enabled-tags: - - diagnostic - disabled-tags: - - performance - - style - - experimental - - opinionated - stylecheck: - checks: ["-ST1000", "-ST1003", "-ST1020", "-ST1021", "-ST1022"] - goheader: - template: |- - Copyright (c) Abstract Machines - SPDX-License-Identifier: Apache-2.0 - linters: - disable-all: true - fast: true + default: none enable: - - gocritic - - gosimple - - errcheck - - govet - - unused - - goconst - - godot - - godox - - ineffassign - - misspell - - stylecheck - - whitespace - - gci - - gofmt - - goimports - - loggercheck - - goheader - asasalint - asciicheck - bidichk - contextcheck + - copyloopvar - decorder - dogsled + - dupword + - errcheck - errchkjson - errname - - execinquery - - exportloopref - ginkgolinter - gocheckcompilerdirectives - - gofumpt + - goconst + - gocritic + - godot + - godox + - goheader - goprintffuncname + - govet - importas + - ineffassign + - loggercheck - makezero - mirror + - misspell - nakedret - - dupword + - staticcheck + - unused + - whitespace + settings: + gocritic: + enabled-checks: + - importShadow + - httpNoBody + - paramTypeCombine + - emptyStringTest + - builtinShadow + - exposedSyncMutex + disabled-checks: + - appendAssign + enabled-tags: + - diagnostic + disabled-tags: + - performance + - style + - experimental + - opinionated + goheader: + template: |- + Copyright (c) Abstract Machines + SPDX-License-Identifier: Apache-2.0 + importas: + no-unaliased: true + no-extra-aliases: false + staticcheck: + checks: + - -ST1000 + - -ST1003 + - -ST1020 + - -ST1021 + - -ST1022 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gci + - gofmt + - gofumpt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/Makefile b/Makefile index 6545ef2..80865ed 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,27 @@ # Copyright (c) Abstract Machines # SPDX-License-Identifier: Apache-2.0 -all: - CGO_ENABLED=0 GOOS=linux go build -ldflags "-s -w" -o build/coap-cli-linux main.go - CGO_ENABLED=0 GOOS=darwin go build -ldflags "-s -w" -o build/coap-cli-darwin main.go - CGO_ENABLED=0 GOOS=windows go build -ldflags "-s -w" -o build/coap-cli-windows main.go +INSTALL_DIR=/usr/local/bin +BUILD_DIR=build +BUILD_FLAGS=-ldflags "-s -w" + +.PHONY: all linux darwin windows install install-linux + +all: linux darwin windows + +linux: + CGO_ENABLED=0 GOOS=linux go build $(BUILD_FLAGS) -o $(BUILD_DIR)/coap-cli-linux main.go + +darwin: + CGO_ENABLED=0 GOOS=darwin go build $(BUILD_FLAGS) -o $(BUILD_DIR)/coap-cli-darwin main.go + +windows: + CGO_ENABLED=0 GOOS=windows go build $(BUILD_FLAGS) -o $(BUILD_DIR)/coap-cli-windows main.go + +install: install-linux + +install-linux: + @cp $(BUILD_DIR)/coap-cli-linux $(INSTALL_DIR)/coap-cli || { echo "Installation failed"; exit 1; } + +clean: + rm -rf $(BUILD_DIR)/* diff --git a/README.md b/README.md index 3ce2d95..1c959b2 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,13 @@ Available Commands: Flags: -a, --auth string Auth + -A, --ca-file string Client CA file + -C, --cert-file string Client certificate file -c, --content-format int Content format (default 50) -h, --help help for coap-cli -H, --host string Host (default "localhost") -k, --keep-alive uint Send a ping after interval seconds of inactivity. If not specified (or 0), keep-alive is disabled (default). + -K, --key-file string Client key file -m, --max-retries uint32 Max retries for keep alive (default 10) -O, --options num,text Add option num with contents of text to the request. If the text begins with 0x, then the hex text (two [0-9a-f] per byte) is converted to binary data. -p, --port string Port (default "5683") @@ -44,10 +47,15 @@ coap-cli get m/aa844fac-2f74-4ec3-8318-849b95d03bcc/c/0bb5ba61-a66e-4972-bab6-26 coap-cli get m/aa844fac-2f74-4ec3-8318-849b95d03bcc/c/0bb5ba61-a66e-4972-bab6-26f19962678f/subtopic --options 6,0x00 --options 15,auth=1e1017e6-dee7-45b4-8a13-00e6afeb66eb ``` +```bash +coap-cli get m/aa844fac-2f74-4ec3-8318-849b95d03bcc/c/0bb5ba61-a66e-4972-bab6-26f19962678f/subtopic --options 6,0x00 --options 15,auth=1e1017e6-dee7-45b4-8a13-00e6afeb66eb --ca-file ssl/certs/ca.crt --cert-file ssl/certs/client.crt --key-file ssl/certs/client.key +``` + ```bash coap-cli post m/aa844fac-2f74-4ec3-8318-849b95d03bcc/c/0bb5ba61-a66e-4972-bab6-26f19962678f/subtopic --auth 1e1017e6-dee7-45b4-8a13-00e6afeb66eb -d "hello world" ``` + ```bash coap-cli post m/aa844fac-2f74-4ec3-8318-849b95d03bcc/c/0bb5ba61-a66e-4972-bab6-26f19962678f/subtopic --auth 1e1017e6-dee7-45b4-8a13-00e6afeb66eb -d "hello world" -H 0.0.0.0 -p 1234 ``` @@ -55,3 +63,7 @@ coap-cli post m/aa844fac-2f74-4ec3-8318-849b95d03bcc/c/0bb5ba61-a66e-4972-bab6-2 ```bash coap-cli post m/aa844fac-2f74-4ec3-8318-849b95d03bcc/c/0bb5ba61-a66e-4972-bab6-26f19962678f/subtopic --options 15,auth=1e1017e6-dee7-45b4-8a13-00e6afeb66eb -d "hello world" -H 0.0.0.0 -p 5683 ``` + +```bash +coap-cli post channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic --auth 1e1017e6-dee7-45b4-8a13-00e6afeb66eb -d "hello world" --ca-file ssl/certs/ca.crt --cert-file ssl/certs/client.crt --key-file ssl/certs/client.key +``` diff --git a/coap/client.go b/coap/client.go index 2395ba2..d2a892d 100644 --- a/coap/client.go +++ b/coap/client.go @@ -11,6 +11,8 @@ import ( "log" "time" + piondtls "github.com/pion/dtls/v3" + "github.com/plgd-dev/go-coap/v3/dtls" "github.com/plgd-dev/go-coap/v3/message" "github.com/plgd-dev/go-coap/v3/message/codes" "github.com/plgd-dev/go-coap/v3/message/pool" @@ -39,12 +41,18 @@ type Client struct { } // NewClient returns new CoAP client connecting it to the server. -func NewClient(addr string, keepAlive uint64, maxRetries uint32) (Client, error) { +func NewClient(addr string, keepAlive uint64, maxRetries uint32, dtlsConfig *piondtls.Config) (Client, error) { var dialOptions []udp.Option if keepAlive > 0 { dialOptions = append(dialOptions, options.WithKeepAlive(maxRetries, time.Duration(keepAlive)*time.Second, onInactive)) } - c, err := udp.Dial(addr, dialOptions...) + var c *client.Conn + var err error + if dtlsConfig != nil { + c, err = dtls.Dial(addr, dtlsConfig, dialOptions...) + } else { + c, err = udp.Dial(addr, dialOptions...) + } if err != nil { return Client{}, errors.Join(errDialFailed, err) } diff --git a/go.mod b/go.mod index 87b8fec..0205d25 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,12 @@ module github.com/absmach/coap-cli -go 1.24 +go 1.25.0 require ( github.com/fatih/color v1.18.0 + github.com/pion/dtls/v3 v3.0.7 github.com/plgd-dev/go-coap/v3 v3.4.0 - github.com/spf13/cobra v1.9.1 + github.com/spf13/cobra v1.10.1 ) require ( @@ -13,14 +14,13 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/pion/dtls/v3 v3.0.6 // indirect github.com/pion/logging v0.2.4 // indirect github.com/pion/transport/v3 v3.0.7 // indirect - github.com/spf13/pflag v1.0.7 // indirect + github.com/spf13/pflag v1.0.10 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/crypto v0.40.0 // indirect - golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect - golang.org/x/net v0.42.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect + golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.34.0 // indirect + golang.org/x/sys v0.35.0 // indirect ) diff --git a/go.sum b/go.sum index 00e63cf..cd6d0c2 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= -github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU= +github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q= +github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= @@ -22,26 +22,26 @@ github.com/plgd-dev/go-coap/v3 v3.4.0/go.mod h1:azpceqoHFeGzzNVm3RX4ox6xKHLOJ+pD github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= -github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= -golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= -golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= +golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 82023b5..7544af2 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,10 @@ package main import ( "context" + "crypto/tls" + "crypto/x509" "encoding/hex" + "errors" "fmt" "log" "os" @@ -17,25 +20,13 @@ import ( coap "github.com/absmach/coap-cli/coap" "github.com/fatih/color" + piondtls "github.com/pion/dtls/v3" coapmsg "github.com/plgd-dev/go-coap/v3/message" "github.com/plgd-dev/go-coap/v3/message/codes" "github.com/plgd-dev/go-coap/v3/message/pool" "github.com/spf13/cobra" ) -var ( - host string - port string - contentFormat int - auth string - observe bool - data string - options []string - keepAlive uint64 - verbose bool - maxRetries uint32 -) - const verboseFmt = `Date: %s Code: %s Type: %s @@ -44,6 +35,8 @@ Message-ID: %d ` func main() { + req := &request{} + rootCmd := &cobra.Command{ Use: "coap-cli [options]", Short: "CLI for CoAP", @@ -54,46 +47,49 @@ func main() { Short: "Perform a GET request on a COAP resource", Example: "coap-cli get m/aa844fac-2f74-4ec3-8318-849b95d03bcc/c/0bb5ba61-a66e-4972-bab6-26f19962678f/subtopic -a 1e1017e6-dee7-45b4-8a13-00e6afeb66eb -H localhost -p 5683 -O 17,50 -o \n" + "coap-cli get m/aa844fac-2f74-4ec3-8318-849b95d03bcc/c/0bb5ba61-a66e-4972-bab6-26f19962678f/subtopic --auth 1e1017e6-dee7-45b4-8a13-00e6afeb66eb --host localhost --port 5683 --options 17,50 --observe", - Run: runCmd(codes.GET), + Run: runCmd(req, codes.GET), } - getCmd.Flags().BoolVarP(&observe, "observe", "o", false, "Observe resource") + getCmd.Flags().BoolVarP(&req.observe, "observe", "o", false, "Observe resource") putCmd := &cobra.Command{ Use: "put ", Short: "Perform a PUT request on a COAP resource", Example: "coap-cli put /test -H coap.me -p 5683 -c 50 -d 'hello, world'\n" + "coap-cli put /test --host coap.me --port 5683 --content-format 50 --data 'hello, world'", - Run: runCmd(codes.PUT), + Run: runCmd(req, codes.PUT), } - putCmd.Flags().StringVarP(&data, "data", "d", "", "Data") + putCmd.Flags().StringVarP(&req.data, "data", "d", "", "Data") postCmd := &cobra.Command{ Use: "post ", Short: "Perform a POST request on a COAP resource", Example: "coap-cli post m/aa844fac-2f74-4ec3-8318-849b95d03bcc/c/0bb5ba61-a66e-4972-bab6-26f19962678f/subtopic -a 1e1017e6-dee7-45b4-8a13-00e6afeb66eb -H localhost -p 5683 -c 50 -d 'hello, world'\n" + "coap-cli post m/aa844fac-2f74-4ec3-8318-849b95d03bcc/c/0bb5ba61-a66e-4972-bab6-26f19962678f/subtopic --auth 1e1017e6-dee7-45b4-8a13-00e6afeb66eb --host localhost --port 5683 --content-format 50 --data 'hello, world'", - Run: runCmd(codes.POST), + Run: runCmd(req, codes.POST), } - postCmd.Flags().StringVarP(&data, "data", "d", "", "Data") + postCmd.Flags().StringVarP(&req.data, "data", "d", "", "Data") deleteCmd := &cobra.Command{ Use: "delete ", Short: "Perform a DELETE request on a COAP resource", Example: "coap-cli delete /test -H coap.me -p 5683 -c 50 -d 'hello, world' -O 17,50\n" + "coap-cli delete /test --host coap.me --port 5683 --content-format 50 --data 'hello, world' --options 17,50", - Run: runCmd(codes.DELETE), + Run: runCmd(req, codes.DELETE), } - deleteCmd.Flags().StringVarP(&data, "data", "d", "", "Data") + deleteCmd.Flags().StringVarP(&req.data, "data", "d", "", "Data") rootCmd.AddCommand(getCmd, putCmd, postCmd, deleteCmd) - rootCmd.PersistentFlags().StringVarP(&host, "host", "H", "localhost", "Host") - rootCmd.PersistentFlags().StringVarP(&port, "port", "p", "5683", "Port") - rootCmd.PersistentFlags().StringVarP(&auth, "auth", "a", "", "Auth") - rootCmd.PersistentFlags().IntVarP(&contentFormat, "content-format", "c", 50, "Content format") - rootCmd.PersistentFlags().StringArrayVarP(&options, "options", "O", []string{}, "Add option num with contents of text to the request. If the text begins with 0x, then the hex text (two [0-9a-f] per byte) is converted to binary data.") - rootCmd.PersistentFlags().Uint64VarP(&keepAlive, "keep-alive", "k", 0, "Send a ping after interval seconds of inactivity. If not specified (or 0), keep-alive is disabled (default).") - rootCmd.PersistentFlags().Uint32VarP(&maxRetries, "max-retries", "m", 10, "Max retries for keep alive") - rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output") + rootCmd.PersistentFlags().StringVarP(&req.host, "host", "H", "localhost", "Host") + rootCmd.PersistentFlags().StringVarP(&req.port, "port", "p", "5683", "Port") + rootCmd.PersistentFlags().StringVarP(&req.auth, "auth", "a", "", "Auth") + rootCmd.PersistentFlags().IntVarP(&req.contentFormat, "content-format", "c", 50, "Content format") + rootCmd.PersistentFlags().StringArrayVarP(&req.options, "options", "O", []string{}, "Add option num with contents of text to the request. If the text begins with 0x, then the hex text (two [0-9a-f] per byte) is converted to binary data.") + rootCmd.PersistentFlags().Uint64VarP(&req.keepAlive, "keep-alive", "k", 0, "Send a ping after interval seconds of inactivity. If not specified (or 0), keep-alive is disabled (default).") + rootCmd.PersistentFlags().Uint32VarP(&req.maxRetries, "max-retries", "m", 10, "Max retries for keep alive") + rootCmd.PersistentFlags().BoolVarP(&req.verbose, "verbose", "v", false, "Verbose output") + rootCmd.PersistentFlags().StringVarP(&req.certFile, "cert-file", "C", "", "Client certificate file") + rootCmd.PersistentFlags().StringVarP(&req.keyFile, "key-file", "K", "", "Client key file") + rootCmd.PersistentFlags().StringVarP(&req.clientCAFile, "ca-file", "A", "", "Client CA file") if err := rootCmd.Execute(); err != nil { log.Fatalf("Error executing command: %v", err) @@ -126,14 +122,18 @@ func printMsg(m *pool.Message, verbose bool) { } } -func makeRequest(code codes.Code, args []string) { - client, err := coap.NewClient(host+":"+port, keepAlive, maxRetries) +func makeRequest(req *request, args []string) { + dtlsConfig, err := req.createDTLSConfig() + if err != nil { + log.Fatalf("Error creating DTLS config: %v", err) + } + client, err := coap.NewClient(req.host+":"+req.port, req.keepAlive, req.maxRetries, dtlsConfig) if err != nil { log.Fatalf("Error coap creating client: %v", err) } var opts coapmsg.Options - for _, optString := range options { + for _, optString := range req.options { opt := strings.Split(optString, ",") if len(opt) < 2 { log.Fatal("Invalid option format") @@ -153,20 +153,20 @@ func makeRequest(code codes.Code, args []string) { opts = append(opts, coapmsg.Option{ID: coapmsg.OptionID(optId), Value: []byte(opt[1])}) } } - if auth != "" { - opts = append(opts, coapmsg.Option{ID: coapmsg.URIQuery, Value: []byte("auth=" + auth)}) + if req.auth != "" { + opts = append(opts, coapmsg.Option{ID: coapmsg.URIQuery, Value: []byte("auth=" + req.auth)}) } if opts.HasOption(coapmsg.Observe) { - if value, _ := opts.GetBytes(coapmsg.Observe); len(value) == 1 && value[0] == 0 && !observe { - observe = true + if value, _ := opts.GetBytes(coapmsg.Observe); len(value) == 1 && value[0] == 0 && !req.observe { + req.observe = true } } - switch code { + switch req.code { case codes.GET: switch { - case observe: - obs, err := client.Receive(args[0], verbose, opts...) + case req.observe: + obs, err := client.Receive(args[0], req.verbose, opts...) if err != nil { log.Fatalf("Error observing resource: %v", err) } @@ -183,28 +183,79 @@ func makeRequest(code codes.Code, args []string) { } log.Fatalf("Observation terminated: %v", err) default: - res, err := client.Send(args[0], code, coapmsg.MediaType(contentFormat), nil, opts...) + res, err := client.Send(args[0], req.code, coapmsg.MediaType(req.contentFormat), nil, opts...) if err != nil { log.Fatalf("Error sending message: %v", err) } - printMsg(res, verbose) + printMsg(res, req.verbose) } default: - pld := strings.NewReader(data) - res, err := client.Send(args[0], code, coapmsg.MediaType(contentFormat), pld, opts...) + pld := strings.NewReader(req.data) + res, err := client.Send(args[0], req.code, coapmsg.MediaType(req.contentFormat), pld, opts...) if err != nil { log.Fatalf("Error sending message: %v", err) } - printMsg(res, verbose) + printMsg(res, req.verbose) } } -func runCmd(code codes.Code) func(cmd *cobra.Command, args []string) { +func runCmd(req *request, code codes.Code) func(cmd *cobra.Command, args []string) { return func(cmd *cobra.Command, args []string) { if len(args) < 1 { fmt.Fprintf(os.Stdout, color.YellowString("\nusage: %s\n\n"), cmd.Use) return } - makeRequest(code, args) + req.code = code + makeRequest(req, args) + } +} + +type request struct { + code codes.Code + host string + port string + contentFormat int + auth string + observe bool + data string + options []string + keepAlive uint64 + verbose bool + maxRetries uint32 + certFile string + keyFile string + clientCAFile string +} + +func (r *request) createDTLSConfig() (*piondtls.Config, error) { + if r.certFile == "" || r.keyFile == "" { + return nil, nil + } + dc := &piondtls.Config{} + cert, err := tls.LoadX509KeyPair(r.certFile, r.keyFile) + if err != nil { + return nil, errors.Join(errors.New("failed to load certificates"), err) + } + dc.Certificates = []tls.Certificate{cert} + rootCA, err := loadCertFile(r.clientCAFile) + if err != nil { + return nil, errors.Join(errors.New("failed to load Client CA"), err) + } + if len(rootCA) > 0 { + if dc.RootCAs == nil { + dc.RootCAs = x509.NewCertPool() + } + if !dc.RootCAs.AppendCertsFromPEM(rootCA) { + return nil, errors.New("failed to append root ca tls.Config") + } + } + dc.InsecureSkipVerify = true + return dc, nil +} + +func loadCertFile(certFile string) ([]byte, error) { + if certFile != "" { + return os.ReadFile(certFile) } + return []byte{}, nil }