Skip to content

Commit

Permalink
feat: add dtls config
Browse files Browse the repository at this point in the history
Signed-off-by: 1998-felix <[email protected]>
  • Loading branch information
felixgateru committed May 24, 2024
1 parent 1a2256f commit e073ad7
Show file tree
Hide file tree
Showing 14 changed files with 446 additions and 60 deletions.
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
```
147 changes: 101 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 @@ -15,27 +18,16 @@ import (
"syscall"
"time"

cli "github.com/absmach/coap-cli"
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 +36,11 @@ Message-ID: %d
`

func main() {
req := request{}
cfg, err := cli.LoadConfig()
if err != nil {
log.Fatalf("Error loading config: %v", err)
}
rootCmd := &cobra.Command{
Use: "coap-cli <method> <URL> [options]",
Short: "CLI for CoAP",
Expand All @@ -54,46 +51,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", cfg.Observe, "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", cfg.Host, "Host")
rootCmd.PersistentFlags().StringVarP(&req.port, "port", "p", cfg.Port, "Port")
rootCmd.PersistentFlags().StringVarP(&req.auth, "auth", "a", cfg.Auth, "Auth")
rootCmd.PersistentFlags().IntVarP(&req.contentFormat, "content-format", "c", cfg.ContentFormat, "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", cfg.KeepAlive, "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", cfg.MaxRetries, "Max retries for keep alive")
rootCmd.PersistentFlags().BoolVarP(&req.verbose, "verbose", "v", cfg.Verbose, "Verbose output")
rootCmd.PersistentFlags().StringVarP(&req.certFile, "cert-file", "C", cfg.CertFile, "Client certificate file")
rootCmd.PersistentFlags().StringVarP(&req.keyFile, "key-file", "K", cfg.KeyFile, "Client key file")
rootCmd.PersistentFlags().StringVarP(&req.clientCAFile, "ca-file", "A", cfg.ClientCAFile, "Client CA file")

if err := rootCmd.Execute(); err != nil {
log.Fatalf("Error executing command: %v", err)
Expand Down Expand Up @@ -126,14 +126,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 +157,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 +187,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
}
11 changes: 10 additions & 1 deletion 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,11 +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))
}
if dtlsConfig != nil {
c, err := dtls.Dial(addr, dtlsConfig, dialOptions...)
if err != nil {
return Client{}, errors.Join(errDialFailed, err)
}
return Client{conn: c}, nil
}
c, err := udp.Dial(addr, dialOptions...)
if err != nil {
return Client{}, errors.Join(errDialFailed, err)
Expand Down
35 changes: 35 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package coapcli

import (
"github.com/spf13/viper"
)

type config struct {
Host string `mapstructure:"host"`
Port string `mapstructure:"port"`
ContentFormat int `mapstructure:"contentFormat"`
Auth string `mapstructure:"auth"`
Observe bool `mapstructure:"observe"`
KeepAlive uint64 `mapstructure:"keep-alive"`
MaxRetries uint32 `mapstructure:"max-retries"`
Verbose bool `mapstructure:"verbose"`
CertFile string `mapstructure:"cert-file"`
KeyFile string `mapstructure:"key-file"`
ClientCAFile string `mapstructure:"ca-file"`
}

func LoadConfig() (config, error) {
viper.SetConfigName("config")
viper.AddConfigPath(".")
viper.SetConfigType("yaml")
err := viper.ReadInConfig()
if err != nil {
return config{}, err
}
var cfg config
err = viper.Unmarshal(&cfg)
if err != nil {
return config{}, err
}
return cfg, nil
}
11 changes: 11 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
host: "localhost"
port: "5683"
auth: ""
contentFormat: 50
keep-alive: 0
max-retries: 10
observe: false
verbose: false
cert-file: ""
key-file: ""
ca-file: ""
Loading

0 comments on commit e073ad7

Please sign in to comment.