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

Feature/27 fin controller #44

Merged
merged 12 commits into from
Mar 14, 2024
9 changes: 9 additions & 0 deletions docker/mqtt/config/mosquitto.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
listener 1883
persistence true
persistence_location /mosquitto/data/
log_dest file /mosquitto/log/mosquitto.log

## Authentication ##
# By default, Mosquitto >=2.0 allows only authenticated connections. Change to true to enable anonymous connections.
allow_anonymous true
# password_file /mosquitto/config/password.txt
Empty file added docker/mqtt/data/.gitkeep
Empty file.
44 changes: 44 additions & 0 deletions docker/mqtt/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
version: '3.7'
services:
mosquitto:
image: eclipse-mosquitto
container_name: mosquitto
volumes:
- type: volume
source: mosquitto_config
target: /mosquitto/config
- type: volume
source: mosquitto_data
target: /mosquitto/data
- type: volume
source: mosquitto_log
target: /mosquitto/log
ports:
- target: 1883
published: 1883
protocol: tcp
mode: host
- target: 9001
published: 9001
protocol: tcp
mode: host

volumes:
mosquitto_config:
driver: local # Define the driver and options under the volume name
driver_opts:
type: none
device: ./config
o: bind
mosquitto_data:
driver: local # Define the driver and options under the volume name
driver_opts:
type: none
device: ./data
o: bind
mosquitto_log:
driver: local # Define the driver and options under the volume name
driver_opts:
type: none
device: ./log
o: bind
Empty file added docker/mqtt/log/.gitkeep
Empty file.
10 changes: 7 additions & 3 deletions docs/content/en/docs/soarca-extentions/fin-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ The goal of the protocol is to provide a simple and robust way to communicate be
## MQTT
To allow for dynamic communication MQTT is used to provide the backbone for the fin communication. SOARCA can be configured using the environment to use MQTT or just run stand alone.

The Fin will use the protocol to register itself to SOARCA via the register message. Once register it will communicate over the channel new channel designated by the capability UUID.
The Fin will use the protocol to register itself to SOARCA via the register message. Once register it will communicate over the channel new channel designated by the fin UUID.

Commands to a specific capability will be communicated of the capability UUID channel.

## Messages
Messages defined in the protocol
Expand Down Expand Up @@ -84,7 +86,8 @@ The message is used to register a fin to SOARCA. It has the following payload.
| ----------------- | --------------------- | ----------------- | ----------- |
|type |register |string |The register message type
|message_id |UUID |string |Message UUID
|fin_id |UUID |string |Fin uuid of the separate form the capability
|fin_id |UUID |string |Fin uuid separate form the capability id
|Name |Name |string |Fin name
|protocol_version |version |string |Version information of the protocol in [semantic version](https://semver.org) schema e.g. 1.2.4-beta
|security |security information |[Security](#security) |Security information for protocol see security structure
|capabilities |list of capability structure |list of [capability structure](#capability-structure) |Capability structure information for protocol see security structure
Expand Down Expand Up @@ -129,6 +132,7 @@ The message is used to register a fin to SOARCA. It has the following payload.
"type": "register",
"message_id": "uuid",
"fin_id" : "uuid",
"name": "Fin name",
"protocol_version": "<semantic-version>",
"security": {
"version": "0.0.0",
Expand All @@ -153,7 +157,7 @@ The message is used to register a fin to SOARCA. It has the following payload.
"agent" : {
"soarca-fin--<uuid>": {
"type": "soarca-fin",
"name": "soarca-fin--<name>-<uuid>"
"name": "soarca-fin--<name>-<capability_uuid>"
}
}

Expand Down
189 changes: 189 additions & 0 deletions internal/capability/controller/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package controller

import (
"errors"
"fmt"
"reflect"
"soarca/internal/fin/protocol"
"soarca/logger"
"soarca/models/fin"

mqtt "github.com/eclipse/paho.mqtt.golang"
)

type Empty struct{}

var component = reflect.TypeOf(Empty{}).PkgPath()
var log *logger.Log

func init() {
log = logger.Logger(component, logger.Info, "", logger.Json)
}

type CapabilityDetails struct {
Name string
Id string
FinId string
}

const clientId = "soarca"

type IFinController interface {
GetRegisteredCapabilities() map[string]CapabilityDetails
}

type FinController struct {
registeredCapabilities map[string]CapabilityDetails
mqttClient mqtt.Client
channel chan []byte
}

func (finController *FinController) GetRegisteredCapabilities() map[string]CapabilityDetails {
return finController.registeredCapabilities
}

func New(client mqtt.Client) *FinController {
controllerQueue := make(chan []byte, 10)
return &FinController{registeredCapabilities: make(map[string]CapabilityDetails), mqttClient: client, channel: controllerQueue}
}

func NewClient(url protocol.Broker, port int) *mqtt.Client {
options := mqtt.NewClientOptions()
options.AddBroker(fmt.Sprintf("mqtt://%s:%d", url, port))
options.SetClientID(clientId)
options.SetUsername("soarca")
options.SetPassword("password")
client := mqtt.NewClient(options)
return &client
}

func (finController *FinController) ConnectAndSubscribe() error {
if finController.mqttClient == nil {
return errors.New("fincontroller mqtt cilent is nil")
}

if token := finController.mqttClient.Connect(); token.Wait() && token.Error() != nil {
err := token.Error()
log.Error(err)
return err
}

token := finController.mqttClient.Subscribe(string("soarca"), 1, finController.Handler)
token.Wait()
if err := token.Error(); err != nil {
return err
}
return nil
}

// This function will only return on a fatal error
func (finController *FinController) Run() {
for {
result := <-finController.channel
finController.Handle(result)
}
}

// Handle goroutine call from mqtt stack
func (finController *FinController) Handler(client mqtt.Client, msg mqtt.Message) {
if msg.Topic() != string("soarca") {
log.Trace("message was receive in wrong topic: " + msg.Topic())
}
payload := msg.Payload()
log.Trace(string(payload))
finController.channel <- payload

}

func (finController *FinController) SendAck(topic string, messageId string) error {
json, _ := fin.Encode(fin.NewAck(messageId))
log.Trace("Sending ack for message id: ", messageId)
token := finController.mqttClient.Publish(topic, 1, false, json)
token.Wait()
if err := token.Error(); err != nil {
log.Error(err)
return err
}
return nil
}

func (finController *FinController) Handle(payload []byte) {
message := fin.Message{}
if err := fin.Decode(payload, &message); err != nil {
log.Error(err)
return
}
switch message.Type {
case fin.MessageTypeAck:
finController.HandleAck(payload)
case fin.MessageTypeRegister:
finController.HandleRegister(payload)
case fin.MessageTypeNack:
finController.HandleNack(payload)
}
}

func (finController *FinController) SendNack(topic string, messageId string) error {
json, _ := fin.Encode(fin.NewNack(messageId))
log.Trace("Sending nack for message id: ", messageId)
token := finController.mqttClient.Publish(topic, 1, false, json)
token.Wait()
if err := token.Error(); err != nil {
log.Error(err)
return err
}
return nil
}

func (finController *FinController) HandleAck(payload []byte) {
ack := fin.Ack{}
if err := fin.Decode(payload, ack); err != nil {
log.Error(err)
}

// ignore for now

}

func (finController *FinController) HandleNack(payload []byte) {
ack := fin.Ack{}
if err := fin.Decode(payload, ack); err != nil {
log.Error(err)
}

// ignore for now

}

func (finController *FinController) HandleRegister(payload []byte) {
register := fin.Register{}
err := fin.Decode(payload, &register)
if err != nil {
log.Error("Message", err)
if err := finController.SendNack("soarca", register.MessageId); err != nil {
log.Error(err)
}
return
}

for _, capability := range register.Capabilities {
if _, ok := finController.registeredCapabilities[capability.Id]; ok {
if err := finController.SendNack(register.FinID, register.MessageId); err != nil {
log.Error(err)
}
log.Error("this capability UUID is already registered")
return
}
token := finController.mqttClient.Subscribe(capability.Id, 1, finController.Handler)
token.Wait()

detail := CapabilityDetails{Name: capability.Name, Id: capability.Id, FinId: register.FinID}
finController.registeredCapabilities[capability.Id] = detail

}

if err := finController.SendAck(register.FinID, register.MessageId); err != nil {
log.Error(err)
}

}
36 changes: 24 additions & 12 deletions models/fin/fin.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,20 @@ type Register struct {
Type string `json:"type"`
MessageId string `json:"message_id"`
FinID string `json:"fin_id"`
Name string `json:"fin_name"`
ProtocolVersion string `json:"protocol_version"`
Security Security `json:"security"`
Capabilities []Capability `json:"capabilities"`
Meta Meta `json:"meta"`
Meta Meta `json:"meta,omitempty"`
}

// Capability register message substructure
type Capability struct {
CapabilityId string `json:"capability_id"`
Name string `json:"name"`
Version string `json:"version"`
Step map[string]Step `json:"step"`
Agent map[string]cacao.AgentTarget `json:"agent"`
Id string `json:"capability_id"`
Name string `json:"name"`
Version string `json:"version"`
Step map[string]Step `json:"step,omitempty"`
Agent map[string]cacao.AgentTarget `json:"agent,omitempty"`
}

// Step structure as example to the executor
Expand All @@ -63,11 +64,11 @@ type Step struct {

// Unregister command structure
type Unregister struct {
Type string `json:"type"`
MessageId string `json:"message_id"`
CapabilityId string `json:"capability_id"`
FinID string `json:"fin_id"`
All string `json:"all"`
Type string `json:"type"`
MessageId string `json:"message_id"`
Id string `json:"capability_id"`
FinID string `json:"fin_id"`
All string `json:"all"`
}

// Command
Expand Down Expand Up @@ -140,7 +141,8 @@ type Meta struct {
}

type Message struct {
Type string `json:"type"`
Type string `json:"type"`
MessageId string `json:"message_id"`
}

func NewCommand() Command {
Expand All @@ -152,6 +154,16 @@ func NewCommand() Command {
return instance
}

func NewAck(messageId string) Ack {
ack := Ack{Type: MessageTypeAck, MessageId: messageId}
return ack
}

func NewNack(messageId string) Nack {
nack := Nack{Type: MessageTypeNack, MessageId: messageId}
return nack
}

func Decode(data []byte, object any) error {
return json.Unmarshal(data, object)
}
Expand Down
36 changes: 36 additions & 0 deletions test/manual/capability/capability_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package capability_controller_test

import (
"fmt"
"soarca/internal/capability/controller"
"testing"

mqtt "github.com/eclipse/paho.mqtt.golang"
)

func TestConnect(t *testing.T) {
// used for manual testing

// var executionId, _ = uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
// var playbookId = "playbook--d09351a2-a075-40c8-8054-0b7c423db83f"
// var stepId = "step--81eff59f-d084-4324-9e0a-59e353dbd28f"
// guid := new(guid.Guid)
// prot := protocol.FinProtocol{Guid: guid, Topic: protocol.Topic("testing"), Broker: "localhost", Port: 1883}

options := mqtt.NewClientOptions()
options.AddBroker("mqtt://localhost:1883")
options.SetClientID("soarca")
options.SetUsername("public")
options.SetPassword("password")

client := mqtt.NewClient(options)

finController := controller.New(client)

if err := finController.ConnectAndSubscribe(); err != nil {
fmt.Print(err)
t.Fail()
}
finController.Run()

}
Loading