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 - Migrate gocoap library from v2 to v3.3 #8

Merged
merged 14 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from 9 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0

# Binaries for programs and plugins
*.exe
*.exe~
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# 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
Expand Down
55 changes: 39 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,52 @@
# CoAP CLI
Simple CoAP cli client written in Go.

Simple CoAP cli client written in Go.

## Usage
Pre-built binary can be found here: https://github.com/mainflux/coap-cli/releases/tag/v0.3.3.
When running, please provide following format:
`coap-cli` followed by method code (`get`, `put`, `post`, `delete`) and CoAP URL. After that, you can pass following flags:

| Flag | Description | Default value |
| ---- | ---------------------------------------------- | ---------------- |
| o | observe option - only valid with GET request | false |
| auth | auth option sent as URI Query | "" |
| h | host | "localhost" |
| p | port | "5683" |
| d | data to be sent in POST or PUT | "" |
| cf | content format | 50 (JSON format) |

```bash
Usage:
coap-cli [command]

Available Commands:
completion Generate the autocompletion script for the specified shell
delete Perform a DELETE request on a COAP resource
get Perform a GET request on a COAP resource
help Help about any command
post Perform a POST request on a COAP resource
put Perform a PUT request on a COAP resource

Flags:
-a, --auth string Auth
-c, --content-format int Content format (default 50)
-h, --help help for coap-cli
-H, --host string Host (default "localhost")
-O, --options stringArray Options
-p, --port string Port (default "5683")
felixgateru marked this conversation as resolved.
Show resolved Hide resolved
-d, --data string Data(default "") - only available for put, post and delete commands
-o, --observe bool Observe - only available for get command

Use "coap-cli [command] --help" for more information about a command
```
The options flag accepts a comma separated string comprising of the optionID defined by [RFC-7252](https://datatracker.ietf.org/doc/html/rfc7252) and a string or hex value. Hex values are used to set options that require numerical values e.g observe, maxAge

## Examples:

```bash
coap-cli get channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic -auth 1e1017e6-dee7-45b4-8a13-00e6afeb66eb -o
coap-cli get channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic --auth 1e1017e6-dee7-45b4-8a13-00e6afeb66eb -o
```

```bash
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 post channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic -auth 1e1017e6-dee7-45b4-8a13-00e6afeb66eb -d "hello world"
coap-cli post channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic --auth 1e1017e6-dee7-45b4-8a13-00e6afeb66eb -d "hello world"
```

```bash
coap-cli post channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic -auth 1e1017e6-dee7-45b4-8a13-00e6afeb66eb -d "hello world" -h 0.0.0.0 -p 1234
coap-cli post channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic --auth 1e1017e6-dee7-45b4-8a13-00e6afeb66eb -d "hello world" -H 0.0.0.0 -p 1234
felixgateru marked this conversation as resolved.
Show resolved Hide resolved
```
```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
```
266 changes: 142 additions & 124 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,163 +5,181 @@ package main

import (
"context"
"errors"
"flag"
"encoding/hex"
"fmt"
"log"
"os"
"os/signal"
"strconv"
"strings"
"syscall"

coap "github.com/absmach/coap-cli/coap"
"github.com/plgd-dev/go-coap/v2/message"
coapmsg "github.com/plgd-dev/go-coap/v2/message"
"github.com/plgd-dev/go-coap/v2/message/codes"
"github.com/plgd-dev/go-coap/v2/udp/message/pool"
)

const (
helpCmd = `Use "coap-cli --help" for help.`
helpMsg = `
Usage: coap-cli <method> <URL> [options]
mathod: get, put, post or delete
-o observe option - only valid with GET request (default: false)
-auth auth option sent as URI Query (default: "")
-h host (default: "localhost")
-p port (default: "5683")
-d data to be sent in POST or PUT (default: "")
-cf content format (default: 50 - JSON format))

Examples:
coap-cli get channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic -auth 1e1017e6-dee7-45b4-8a13-00e6afeb66eb -o
coap-cli post channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic -auth 1e1017e6-dee7-45b4-8a13-00e6afeb66eb -d "hello world"
coap-cli post channels/0bb5ba61-a66e-4972-bab6-26f19962678f/messages/subtopic -auth 1e1017e6-dee7-45b4-8a13-00e6afeb66eb -d "hello world" -h 0.0.0.0 -p 1234
`
"github.com/fatih/color"
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 (
errCreateClient = errors.New("failed to create client")
errSendMessage = errors.New("failed to send message")
errInvalidObsOpt = errors.New("invalid observe option")
errFailedObserve = errors.New("failed to observe resource")
errTerminatedObs = errors.New("observation terminated")
errCancelObs = errors.New("failed to cancel observation")
errCodeNotSupported = errors.New("message can be GET, POST, PUT or DELETE")
host string
port string
contentFormat int
auth string
observe bool
data string
options []string
keepAlive uint64
)

type request struct {
code codes.Code
path string
host *string
port *string
cf *int
data *string
auth *string
obs *bool
}

func parseCode(code string) (codes.Code, error) {
ret, err := codes.ToCode(code)
if err != nil {
return 0, err
}
switch ret {
case codes.GET, codes.POST, codes.PUT, codes.DELETE:
return ret, nil
default:
return 0, errCodeNotSupported
func main() {
rootCmd := &cobra.Command{
Use: "coap-cli <method> <URL> [options]",
Short: "CLI for CoAP",
}

getCmd := &cobra.Command{
Use: "get <url>",
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),
}
getCmd.Flags().BoolVarP(&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),
}
putCmd.Flags().StringVarP(&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),
}
postCmd.Flags().StringVarP(&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),
}
deleteCmd.Flags().StringVarP(&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{}, "Options")
felixgateru marked this conversation as resolved.
Show resolved Hide resolved
rootCmd.PersistentFlags().Uint64VarP(&keepAlive, "keep-alive", "K", 60, "Keep alive interval")
felixgateru marked this conversation as resolved.
Show resolved Hide resolved

if err := rootCmd.Execute(); err != nil {
log.Fatalf("Error executing command: %v", err)
}
}

func printMsg(m *pool.Message) {
felixgateru marked this conversation as resolved.
Show resolved Hide resolved
if m != nil {
log.Printf("\nMESSAGE:\n %v", m)
log.Printf("\nMESSAGE:\n%s", m.String())
}
}

func main() {
if len(os.Args) < 2 {
log.Fatal(helpCmd)
}
help := strings.ToLower(os.Args[1])
if help == "-h" || help == "--help" {
log.Print(helpMsg)
return
}
req := request{}
var err error
req.code, err = parseCode(strings.ToUpper(os.Args[1]))
body, err := m.ReadBody()
if err != nil {
log.Fatalf("Can't read request code: %s\n%s", err, helpCmd)
}

if len(os.Args) < 3 {
log.Fatalf("CoAP URL must not be empty.\n%s", helpCmd)
log.Fatalf("failed to read body %v", err)
}
req.path = os.Args[2]
if strings.HasPrefix(req.path, "-") {
log.Fatalf("Please enter a valid CoAP URL.\n%s", helpCmd)
}

os.Args = os.Args[2:]
req.obs = flag.Bool("o", false, "Observe")
req.host = flag.String("h", "localhost", "Host")
req.port = flag.String("p", "5683", "Port")
// Default type is JSON.
req.cf = flag.Int("cf", 50, "Content format")
req.data = flag.String("d", "", "Message data")
req.auth = flag.String("auth", "", "Auth token")
flag.Parse()

if err = makeRequest(req); err != nil {
log.Fatal(err)
if len(body) > 0 {
log.Printf("\nMESSAGE BODY:\n%s", string(body))
}
}

func makeRequest(req request) error {
client, err := coap.New(*req.host + ":" + *req.port)
func makeRequest(code codes.Code, args []string) {
client, err := coap.New(host+":"+port, keepAlive)
if err != nil {
return errors.Join(errCreateClient, err)
}
var opts coapmsg.Options
if req.auth != nil {
opts = append(opts, coapmsg.Option{ID: coapmsg.URIQuery, Value: []byte(fmt.Sprintf("auth=%s", *req.auth))})
log.Fatalf("Error coap creating client: %v", err)
}

if req.obs == nil || (!*req.obs) {
pld := strings.NewReader(*req.data)

res, err := client.Send(req.path, req.code, message.MediaType(*req.cf), pld, opts...)
var opts coapmsg.Options
for _, optString := range options {
opt := strings.Split(optString, ",")
if len(opt) < 2 {
log.Fatal("Invalid option format")
}
optId, err := strconv.ParseUint(opt[0], 10, 16)
if err != nil {
return errors.Join(errSendMessage, err)
log.Fatal("Error parsing option id")
}
if strings.HasPrefix(opt[1], "0x") {
op := strings.TrimPrefix(opt[1], "0x")
optValue, err := hex.DecodeString(op)
if err != nil {
log.Fatal("Invalid option value ", err.Error())
}
opts = append(opts, coapmsg.Option{ID: coapmsg.OptionID(optId), Value: optValue})
} else {
opts = append(opts, coapmsg.Option{ID: coapmsg.OptionID(optId), Value: []byte(opt[1])})
}
printMsg(res)
return nil
}
if req.code != codes.GET {
return errInvalidObsOpt
if auth != "" {
opts = append(opts, coapmsg.Option{ID: coapmsg.URIQuery, Value: []byte("auth=" + auth)})
}
felixgateru marked this conversation as resolved.
Show resolved Hide resolved
obs, err := client.Receive(req.path, opts...)
if err != nil {
return errors.Join(errFailedObserve, err)
if opts.HasOption(coapmsg.Observe) {
if value, _ := opts.GetBytes(coapmsg.Observe); len(value) == 1 && value[0] == 0 && !observe {
observe = true
}
}

errs := make(chan error, 1)
go func() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT)

sig := <-sigChan
errs <- fmt.Errorf("%v", sig)
}()

err = <-errs
if err != nil {
return errors.Join(errTerminatedObs, err)
switch code {
case codes.GET:
switch {
case observe:
obs, err := client.Receive(args[0], opts...)
if err != nil {
log.Fatalf("Error observing resource: %v", err)
}
errs := make(chan error, 2)
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()

err = <-errs
if err := obs.Cancel(context.Background(), opts...); err != nil {
log.Fatalf("Error cancelling observation: %v", err)
}
log.Fatalf("Observation terminated: %v", err)
default:
res, err := client.Send(args[0], code, coapmsg.MediaType(contentFormat), nil, opts...)
if err != nil {
log.Fatalf("Error sending message: %v", err)
}
printMsg(res)
}
default:
pld := strings.NewReader(data)
res, err := client.Send(args[0], code, coapmsg.MediaType(contentFormat), pld, opts...)
if err != nil {
log.Fatalf("Error sending message: %v", err)
}
printMsg(res)
}
if err := obs.Cancel(context.Background()); err != nil {
return errors.Join(errCancelObs, err)
}

func runCmd(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)
}
return nil
}
Loading
Loading