Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NOISSUE - Add DTLS config to coap-cli #11

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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 cmd/main.go
CGO_ENABLED=0 GOOS=darwin go build -ldflags "-s -w" -o build/coap-cli-darwin cmd/main.go
CGO_ENABLED=0 GOOS=windows go build -ldflags "-s -w" -o build/coap-cli-windows cmd/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 cmd/main.go

darwin:
CGO_ENABLED=0 GOOS=darwin go build $(BUILD_FLAGS) -o $(BUILD_DIR)/coap-cli-darwin cmd/main.go

windows:
CGO_ENABLED=0 GOOS=windows go build $(BUILD_FLAGS) -o $(BUILD_DIR)/coap-cli-windows cmd/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)/*
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -44,6 +47,10 @@ coap-cli get channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic --a
coap-cli get channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic --options 6,0x00 --options 15,auth=1e1017e6-dee7-45b4-8a13-00e6afeb66eb
```

```bash
coap-cli get channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/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 channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic --auth 1e1017e6-dee7-45b4-8a13-00e6afeb66eb -d "hello world"
```
Expand All @@ -55,3 +62,6 @@ coap-cli post channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic --
```bash
coap-cli post channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/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
```
143 changes: 97 additions & 46 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ package main

import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"errors"
"fmt"
"log"
"os"
Expand All @@ -17,25 +20,13 @@ import (

coap "github.com/absmach/coap-cli/coap"
"github.com/fatih/color"
piondtls "github.com/pion/dtls/v2"
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
Expand All @@ -44,6 +35,8 @@ Message-ID: %d
`

func main() {
req := &request{}

rootCmd := &cobra.Command{
Use: "coap-cli <method> <URL> [options]",
Short: "CLI for CoAP",
Expand All @@ -54,46 +47,49 @@ func main() {
Short: "Perform a GET request on a COAP resource",
Example: "coap-cli get channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic -a 1e1017e6-dee7-45b4-8a13-00e6afeb66eb -H localhost -p 5683 -O 17,50 -o \n" +
"coap-cli get channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/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 <url>",
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 <url>",
Short: "Perform a POST request on a COAP resource",
Example: "coap-cli post channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic -a 1e1017e6-dee7-45b4-8a13-00e6afeb66eb -H localhost -p 5683 -c 50 -d 'hello, world'\n" +
"coap-cli post channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/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 <url>",
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)
Expand Down Expand Up @@ -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")
Expand All @@ -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)
}
Expand All @@ -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
}
12 changes: 10 additions & 2 deletions coap/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"log"
"time"

piondtls "github.com/pion/dtls/v2"
"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"
Expand Down Expand Up @@ -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)
}
Expand Down
40 changes: 40 additions & 0 deletions examples/coapme/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
host="coap.me"
port="5683"
testPath="/test"
largePath="/large"
etagPath="/etag"
payload="Hello world from absmach/coap-cli"

#Get
echo "Sending GET request to $host:$port$testPath"
coap-cli get $testPath -H $host -p $port
sleep 1

#Get with blockwise transfer
echo "Sending GET request with blockwise transfer to $host:$port$testPath"
coap-cli get $largePath -H $host -p $port
sleep 1

#Post
echo "Sending POST request to $host:$port$testPath"
coap-cli post $testPath -H $host -p $port -d $payload
sleep 1

#Post with content format
echo "Sending POST request with content format to $host:$port$testPath"
coap-cli post $testPath -H $host -p $port -d $payload -c 50
sleep 1

#Post with authentication
echo "Sending POST request with authentication to $host:$port$testPath"
coap-cli post $testPath -H $host -p $port -d $payload --auth "test"
sleep 1

#Put
echo "Sending PUT request to $host:$port$testPath"
coap-cli put $testPath -H $host -p $port -d $payload
sleep 1

#Delete
echo "Sending DELETE request to $host:$port$testPath"
coap-cli delete $testPath -H $host -p $port
Loading