Skip to content

Commit e073ad7

Browse files
committed
feat: add dtls config
Signed-off-by: 1998-felix <felix.gateru@gmail.com>
1 parent 1a2256f commit e073ad7

File tree

14 files changed

+446
-60
lines changed

14 files changed

+446
-60
lines changed

Makefile

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,27 @@
11
# Copyright (c) Abstract Machines
22
# SPDX-License-Identifier: Apache-2.0
33

4-
all:
5-
CGO_ENABLED=0 GOOS=linux go build -ldflags "-s -w" -o build/coap-cli-linux cmd/main.go
6-
CGO_ENABLED=0 GOOS=darwin go build -ldflags "-s -w" -o build/coap-cli-darwin cmd/main.go
7-
CGO_ENABLED=0 GOOS=windows go build -ldflags "-s -w" -o build/coap-cli-windows cmd/main.go
4+
INSTALL_DIR=/usr/local/bin
5+
BUILD_DIR=build
6+
BUILD_FLAGS=-ldflags "-s -w"
7+
8+
.PHONY: all linux darwin windows install install-linux
9+
10+
all: linux darwin windows
11+
12+
linux:
13+
CGO_ENABLED=0 GOOS=linux go build $(BUILD_FLAGS) -o $(BUILD_DIR)/coap-cli-linux cmd/main.go
14+
15+
darwin:
16+
CGO_ENABLED=0 GOOS=darwin go build $(BUILD_FLAGS) -o $(BUILD_DIR)/coap-cli-darwin cmd/main.go
17+
18+
windows:
19+
CGO_ENABLED=0 GOOS=windows go build $(BUILD_FLAGS) -o $(BUILD_DIR)/coap-cli-windows cmd/main.go
20+
21+
install: install-linux
22+
23+
install-linux:
24+
@cp $(BUILD_DIR)/coap-cli-linux $(INSTALL_DIR)/coap-cli || { echo "Installation failed"; exit 1; }
25+
26+
clean:
27+
rm -rf $(BUILD_DIR)/*

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ Available Commands:
1818

1919
Flags:
2020
-a, --auth string Auth
21+
-A, --ca-file string Client CA file
22+
-C, --cert-file string Client certificate file
2123
-c, --content-format int Content format (default 50)
2224
-h, --help help for coap-cli
2325
-H, --host string Host (default "localhost")
2426
-k, --keep-alive uint Send a ping after interval seconds of inactivity. If not specified (or 0), keep-alive is disabled (default).
27+
-K, --key-file string Client key file
2528
-m, --max-retries uint32 Max retries for keep alive (default 10)
2629
-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.
2730
-p, --port string Port (default "5683")
@@ -44,6 +47,10 @@ coap-cli get channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic --a
4447
coap-cli get channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic --options 6,0x00 --options 15,auth=1e1017e6-dee7-45b4-8a13-00e6afeb66eb
4548
```
4649

50+
```bash
51+
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
52+
```
53+
4754
```bash
4855
coap-cli post channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic --auth 1e1017e6-dee7-45b4-8a13-00e6afeb66eb -d "hello world"
4956
```
@@ -55,3 +62,6 @@ coap-cli post channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic --
5562
```bash
5663
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
5764
```
65+
```bash
66+
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
67+
```

cmd/main.go

Lines changed: 101 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ package main
55

66
import (
77
"context"
8+
"crypto/tls"
9+
"crypto/x509"
810
"encoding/hex"
11+
"errors"
912
"fmt"
1013
"log"
1114
"os"
@@ -15,27 +18,16 @@ import (
1518
"syscall"
1619
"time"
1720

21+
cli "github.com/absmach/coap-cli"
1822
coap "github.com/absmach/coap-cli/coap"
1923
"github.com/fatih/color"
24+
piondtls "github.com/pion/dtls/v2"
2025
coapmsg "github.com/plgd-dev/go-coap/v3/message"
2126
"github.com/plgd-dev/go-coap/v3/message/codes"
2227
"github.com/plgd-dev/go-coap/v3/message/pool"
2328
"github.com/spf13/cobra"
2429
)
2530

26-
var (
27-
host string
28-
port string
29-
contentFormat int
30-
auth string
31-
observe bool
32-
data string
33-
options []string
34-
keepAlive uint64
35-
verbose bool
36-
maxRetries uint32
37-
)
38-
3931
const verboseFmt = `Date: %s
4032
Code: %s
4133
Type: %s
@@ -44,6 +36,11 @@ Message-ID: %d
4436
`
4537

4638
func main() {
39+
req := request{}
40+
cfg, err := cli.LoadConfig()
41+
if err != nil {
42+
log.Fatalf("Error loading config: %v", err)
43+
}
4744
rootCmd := &cobra.Command{
4845
Use: "coap-cli <method> <URL> [options]",
4946
Short: "CLI for CoAP",
@@ -54,46 +51,49 @@ func main() {
5451
Short: "Perform a GET request on a COAP resource",
5552
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" +
5653
"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",
57-
Run: runCmd(codes.GET),
54+
Run: runCmd(req, codes.GET),
5855
}
59-
getCmd.Flags().BoolVarP(&observe, "observe", "o", false, "Observe resource")
56+
getCmd.Flags().BoolVarP(&req.observe, "observe", "o", cfg.Observe, "Observe resource")
6057

6158
putCmd := &cobra.Command{
6259
Use: "put <url>",
6360
Short: "Perform a PUT request on a COAP resource",
6461
Example: "coap-cli put /test -H coap.me -p 5683 -c 50 -d 'hello, world'\n" +
6562
"coap-cli put /test --host coap.me --port 5683 --content-format 50 --data 'hello, world'",
66-
Run: runCmd(codes.PUT),
63+
Run: runCmd(req, codes.PUT),
6764
}
68-
putCmd.Flags().StringVarP(&data, "data", "d", "", "Data")
65+
putCmd.Flags().StringVarP(&req.data, "data", "d", "", "Data")
6966

7067
postCmd := &cobra.Command{
7168
Use: "post <url>",
7269
Short: "Perform a POST request on a COAP resource",
7370
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" +
7471
"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'",
75-
Run: runCmd(codes.POST),
72+
Run: runCmd(req, codes.POST),
7673
}
77-
postCmd.Flags().StringVarP(&data, "data", "d", "", "Data")
74+
postCmd.Flags().StringVarP(&req.data, "data", "d", "", "Data")
7875

7976
deleteCmd := &cobra.Command{
8077
Use: "delete <url>",
8178
Short: "Perform a DELETE request on a COAP resource",
8279
Example: "coap-cli delete /test -H coap.me -p 5683 -c 50 -d 'hello, world' -O 17,50\n" +
8380
"coap-cli delete /test --host coap.me --port 5683 --content-format 50 --data 'hello, world' --options 17,50",
84-
Run: runCmd(codes.DELETE),
81+
Run: runCmd(req, codes.DELETE),
8582
}
86-
deleteCmd.Flags().StringVarP(&data, "data", "d", "", "Data")
83+
deleteCmd.Flags().StringVarP(&req.data, "data", "d", "", "Data")
8784

8885
rootCmd.AddCommand(getCmd, putCmd, postCmd, deleteCmd)
89-
rootCmd.PersistentFlags().StringVarP(&host, "host", "H", "localhost", "Host")
90-
rootCmd.PersistentFlags().StringVarP(&port, "port", "p", "5683", "Port")
91-
rootCmd.PersistentFlags().StringVarP(&auth, "auth", "a", "", "Auth")
92-
rootCmd.PersistentFlags().IntVarP(&contentFormat, "content-format", "c", 50, "Content format")
93-
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.")
94-
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).")
95-
rootCmd.PersistentFlags().Uint32VarP(&maxRetries, "max-retries", "m", 10, "Max retries for keep alive")
96-
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output")
86+
rootCmd.PersistentFlags().StringVarP(&req.host, "host", "H", cfg.Host, "Host")
87+
rootCmd.PersistentFlags().StringVarP(&req.port, "port", "p", cfg.Port, "Port")
88+
rootCmd.PersistentFlags().StringVarP(&req.auth, "auth", "a", cfg.Auth, "Auth")
89+
rootCmd.PersistentFlags().IntVarP(&req.contentFormat, "content-format", "c", cfg.ContentFormat, "Content format")
90+
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.")
91+
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).")
92+
rootCmd.PersistentFlags().Uint32VarP(&req.maxRetries, "max-retries", "m", cfg.MaxRetries, "Max retries for keep alive")
93+
rootCmd.PersistentFlags().BoolVarP(&req.verbose, "verbose", "v", cfg.Verbose, "Verbose output")
94+
rootCmd.PersistentFlags().StringVarP(&req.certFile, "cert-file", "C", cfg.CertFile, "Client certificate file")
95+
rootCmd.PersistentFlags().StringVarP(&req.keyFile, "key-file", "K", cfg.KeyFile, "Client key file")
96+
rootCmd.PersistentFlags().StringVarP(&req.clientCAFile, "ca-file", "A", cfg.ClientCAFile, "Client CA file")
9797

9898
if err := rootCmd.Execute(); err != nil {
9999
log.Fatalf("Error executing command: %v", err)
@@ -126,14 +126,18 @@ func printMsg(m *pool.Message, verbose bool) {
126126
}
127127
}
128128

129-
func makeRequest(code codes.Code, args []string) {
130-
client, err := coap.NewClient(host+":"+port, keepAlive, maxRetries)
129+
func makeRequest(req request, args []string) {
130+
dtlsConfig, err := req.createDTLSConfig()
131+
if err != nil {
132+
log.Fatalf("Error creating DTLS config: %v", err)
133+
}
134+
client, err := coap.NewClient(req.host+":"+req.port, req.keepAlive, req.maxRetries, dtlsConfig)
131135
if err != nil {
132136
log.Fatalf("Error coap creating client: %v", err)
133137
}
134138

135139
var opts coapmsg.Options
136-
for _, optString := range options {
140+
for _, optString := range req.options {
137141
opt := strings.Split(optString, ",")
138142
if len(opt) < 2 {
139143
log.Fatal("Invalid option format")
@@ -153,20 +157,20 @@ func makeRequest(code codes.Code, args []string) {
153157
opts = append(opts, coapmsg.Option{ID: coapmsg.OptionID(optId), Value: []byte(opt[1])})
154158
}
155159
}
156-
if auth != "" {
157-
opts = append(opts, coapmsg.Option{ID: coapmsg.URIQuery, Value: []byte("auth=" + auth)})
160+
if req.auth != "" {
161+
opts = append(opts, coapmsg.Option{ID: coapmsg.URIQuery, Value: []byte("auth=" + req.auth)})
158162
}
159163
if opts.HasOption(coapmsg.Observe) {
160-
if value, _ := opts.GetBytes(coapmsg.Observe); len(value) == 1 && value[0] == 0 && !observe {
161-
observe = true
164+
if value, _ := opts.GetBytes(coapmsg.Observe); len(value) == 1 && value[0] == 0 && !req.observe {
165+
req.observe = true
162166
}
163167
}
164168

165-
switch code {
169+
switch req.code {
166170
case codes.GET:
167171
switch {
168-
case observe:
169-
obs, err := client.Receive(args[0], verbose, opts...)
172+
case req.observe:
173+
obs, err := client.Receive(args[0], req.verbose, opts...)
170174
if err != nil {
171175
log.Fatalf("Error observing resource: %v", err)
172176
}
@@ -183,28 +187,79 @@ func makeRequest(code codes.Code, args []string) {
183187
}
184188
log.Fatalf("Observation terminated: %v", err)
185189
default:
186-
res, err := client.Send(args[0], code, coapmsg.MediaType(contentFormat), nil, opts...)
190+
res, err := client.Send(args[0], req.code, coapmsg.MediaType(req.contentFormat), nil, opts...)
187191
if err != nil {
188192
log.Fatalf("Error sending message: %v", err)
189193
}
190-
printMsg(res, verbose)
194+
printMsg(res, req.verbose)
191195
}
192196
default:
193-
pld := strings.NewReader(data)
194-
res, err := client.Send(args[0], code, coapmsg.MediaType(contentFormat), pld, opts...)
197+
pld := strings.NewReader(req.data)
198+
res, err := client.Send(args[0], req.code, coapmsg.MediaType(req.contentFormat), pld, opts...)
195199
if err != nil {
196200
log.Fatalf("Error sending message: %v", err)
197201
}
198-
printMsg(res, verbose)
202+
printMsg(res, req.verbose)
199203
}
200204
}
201205

202-
func runCmd(code codes.Code) func(cmd *cobra.Command, args []string) {
206+
func runCmd(req request, code codes.Code) func(cmd *cobra.Command, args []string) {
203207
return func(cmd *cobra.Command, args []string) {
204208
if len(args) < 1 {
205209
fmt.Fprintf(os.Stdout, color.YellowString("\nusage: %s\n\n"), cmd.Use)
206210
return
207211
}
208-
makeRequest(code, args)
212+
req.code = code
213+
makeRequest(req, args)
214+
}
215+
}
216+
217+
type request struct {
218+
code codes.Code
219+
host string
220+
port string
221+
contentFormat int
222+
auth string
223+
observe bool
224+
data string
225+
options []string
226+
keepAlive uint64
227+
verbose bool
228+
maxRetries uint32
229+
certFile string
230+
keyFile string
231+
clientCAFile string
232+
}
233+
234+
func (r *request) createDTLSConfig() (*piondtls.Config, error) {
235+
if r.certFile == "" || r.keyFile == "" {
236+
return nil, nil
237+
}
238+
dc := &piondtls.Config{}
239+
cert, err := tls.LoadX509KeyPair(r.certFile, r.keyFile)
240+
if err != nil {
241+
return nil, errors.Join(errors.New("failed to load certificates"), err)
242+
}
243+
dc.Certificates = []tls.Certificate{cert}
244+
rootCA, err := loadCertFile(r.clientCAFile)
245+
if err != nil {
246+
return nil, errors.Join(errors.New("failed to load Client CA"), err)
247+
}
248+
if len(rootCA) > 0 {
249+
if dc.RootCAs == nil {
250+
dc.RootCAs = x509.NewCertPool()
251+
}
252+
if !dc.RootCAs.AppendCertsFromPEM(rootCA) {
253+
return nil, errors.New("failed to append root ca tls.Config")
254+
}
255+
}
256+
dc.InsecureSkipVerify = true
257+
return dc, nil
258+
}
259+
260+
func loadCertFile(certFile string) ([]byte, error) {
261+
if certFile != "" {
262+
return os.ReadFile(certFile)
209263
}
264+
return []byte{}, nil
210265
}

coap/client.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"log"
1212
"time"
1313

14+
piondtls "github.com/pion/dtls/v2"
15+
"github.com/plgd-dev/go-coap/v3/dtls"
1416
"github.com/plgd-dev/go-coap/v3/message"
1517
"github.com/plgd-dev/go-coap/v3/message/codes"
1618
"github.com/plgd-dev/go-coap/v3/message/pool"
@@ -39,11 +41,18 @@ type Client struct {
3941
}
4042

4143
// NewClient returns new CoAP client connecting it to the server.
42-
func NewClient(addr string, keepAlive uint64, maxRetries uint32) (Client, error) {
44+
func NewClient(addr string, keepAlive uint64, maxRetries uint32, dtlsConfig *piondtls.Config) (Client, error) {
4345
var dialOptions []udp.Option
4446
if keepAlive > 0 {
4547
dialOptions = append(dialOptions, options.WithKeepAlive(maxRetries, time.Duration(keepAlive)*time.Second, onInactive))
4648
}
49+
if dtlsConfig != nil {
50+
c, err := dtls.Dial(addr, dtlsConfig, dialOptions...)
51+
if err != nil {
52+
return Client{}, errors.Join(errDialFailed, err)
53+
}
54+
return Client{conn: c}, nil
55+
}
4756
c, err := udp.Dial(addr, dialOptions...)
4857
if err != nil {
4958
return Client{}, errors.Join(errDialFailed, err)

config.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package coapcli
2+
3+
import (
4+
"github.com/spf13/viper"
5+
)
6+
7+
type config struct {
8+
Host string `mapstructure:"host"`
9+
Port string `mapstructure:"port"`
10+
ContentFormat int `mapstructure:"contentFormat"`
11+
Auth string `mapstructure:"auth"`
12+
Observe bool `mapstructure:"observe"`
13+
KeepAlive uint64 `mapstructure:"keep-alive"`
14+
MaxRetries uint32 `mapstructure:"max-retries"`
15+
Verbose bool `mapstructure:"verbose"`
16+
CertFile string `mapstructure:"cert-file"`
17+
KeyFile string `mapstructure:"key-file"`
18+
ClientCAFile string `mapstructure:"ca-file"`
19+
}
20+
21+
func LoadConfig() (config, error) {
22+
viper.SetConfigName("config")
23+
viper.AddConfigPath(".")
24+
viper.SetConfigType("yaml")
25+
err := viper.ReadInConfig()
26+
if err != nil {
27+
return config{}, err
28+
}
29+
var cfg config
30+
err = viper.Unmarshal(&cfg)
31+
if err != nil {
32+
return config{}, err
33+
}
34+
return cfg, nil
35+
}

config.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
host: "localhost"
2+
port: "5683"
3+
auth: ""
4+
contentFormat: 50
5+
keep-alive: 0
6+
max-retries: 10
7+
observe: false
8+
verbose: false
9+
cert-file: ""
10+
key-file: ""
11+
ca-file: ""

0 commit comments

Comments
 (0)