190 lines
5.0 KiB
Go
190 lines
5.0 KiB
Go
package ha
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.i3omb.com/gronod/ha-n95-local-control/internal/config"
|
|
"git.i3omb.com/gronod/ha-n95-local-control/internal/robot"
|
|
)
|
|
|
|
type sendCommandJSON struct {
|
|
Command string `json:"command"`
|
|
CleanType string `json:"clean_type"`
|
|
Action string `json:"action"`
|
|
Name string `json:"name"`
|
|
On string `json:"on"`
|
|
Time string `json:"time"`
|
|
Repeat string `json:"repeat"`
|
|
XML string `json:"xml"`
|
|
}
|
|
|
|
func SendCommandHook(cfg config.Config) func(ctx context.Context, serial string, payload []byte) (robot.Command, error) {
|
|
return func(ctx context.Context, serial string, payload []byte) (robot.Command, error) {
|
|
if len(payload) > 4096 {
|
|
return robot.Command{}, errors.New("rejected:size")
|
|
}
|
|
|
|
var req sendCommandJSON
|
|
if err := json.Unmarshal(payload, &req); err != nil {
|
|
return robot.Command{}, errors.New("rejected:payload")
|
|
}
|
|
|
|
if req.Command == "" {
|
|
return robot.Command{}, errors.New("rejected:payload")
|
|
}
|
|
|
|
switch req.Command {
|
|
case "clean":
|
|
if req.CleanType != "auto" && req.CleanType != "border" && req.CleanType != "spot" && req.CleanType != "singleRoom" {
|
|
return robot.Command{}, fmt.Errorf("rejected:clean-type")
|
|
}
|
|
return robot.Command{Name: "clean", Args: map[string]string{"type": req.CleanType}}, nil
|
|
|
|
case "move":
|
|
if req.Action == "backward" {
|
|
return robot.Command{}, errors.New("rejected:backward")
|
|
}
|
|
if req.Action != "forward" && req.Action != "SpinLeft" && req.Action != "SpinRight" && req.Action != "TurnAround" && req.Action != "stop" {
|
|
return robot.Command{}, errors.New("rejected:move-action")
|
|
}
|
|
return robot.Command{Name: "move", Args: map[string]string{"action": req.Action}}, nil
|
|
|
|
case "cancel_return":
|
|
return robot.Command{Name: "cancel_return"}, nil
|
|
|
|
case "set_time":
|
|
return robot.Command{Name: "set_time"}, nil
|
|
|
|
case "get_status":
|
|
return robot.Command{Name: "get_status"}, nil
|
|
|
|
case "get_lifespan":
|
|
return robot.Command{Name: "get_lifespan"}, nil
|
|
|
|
case "add_sched", "mod_sched":
|
|
if req.Name == "" || len(req.Name) > 64 {
|
|
return robot.Command{}, errors.New("rejected:name")
|
|
}
|
|
if req.On != "1" && req.On != "0" && req.On != "true" && req.On != "false" {
|
|
return robot.Command{}, errors.New("rejected:on")
|
|
}
|
|
if req.On == "true" {
|
|
req.On = "1"
|
|
} else if req.On == "false" {
|
|
req.On = "0"
|
|
}
|
|
if req.CleanType != "auto" {
|
|
return robot.Command{}, errors.New("rejected:schedule-type")
|
|
}
|
|
if len(req.Repeat) != 7 {
|
|
return robot.Command{}, errors.New("rejected:repeat")
|
|
}
|
|
for _, c := range req.Repeat {
|
|
if c != '0' && c != '1' {
|
|
return robot.Command{}, errors.New("rejected:repeat")
|
|
}
|
|
}
|
|
if len(req.Time) != 5 || req.Time[2] != ':' {
|
|
return robot.Command{}, errors.New("rejected:time")
|
|
}
|
|
return robot.Command{
|
|
Name: req.Command,
|
|
Args: map[string]string{
|
|
"name": req.Name,
|
|
"on": req.On,
|
|
"time": req.Time,
|
|
"repeat": req.Repeat,
|
|
},
|
|
}, nil
|
|
|
|
case "del_sched":
|
|
return robot.Command{Name: "del_sched", Args: map[string]string{"name": req.Name}}, nil
|
|
|
|
case "get_sched":
|
|
return robot.Command{Name: "get_sched"}, nil
|
|
|
|
case "resume":
|
|
return robot.Command{}, errors.New("rejected:resume")
|
|
|
|
case "raw":
|
|
if !cfg.RawCommands {
|
|
return robot.Command{}, errors.New("raw-disabled")
|
|
}
|
|
if len(req.XML) > 4096 {
|
|
return robot.Command{}, errors.New("rejected:size")
|
|
}
|
|
dec := xml.NewDecoder(strings.NewReader(req.XML))
|
|
tok, err := dec.Token()
|
|
if err != nil {
|
|
return robot.Command{}, errors.New("rejected:raw-xml")
|
|
}
|
|
var start xml.StartElement
|
|
var ok bool
|
|
if start, ok = tok.(xml.StartElement); !ok {
|
|
return robot.Command{}, errors.New("rejected:raw-xml")
|
|
}
|
|
|
|
// Reject forbidden tags
|
|
switch start.Name.Local {
|
|
case "stream", "auth", "iq", "presence", "message", "starttls", "bind", "session":
|
|
return robot.Command{}, errors.New("rejected:raw-xml-root")
|
|
}
|
|
|
|
if start.Name.Local != "ctl" {
|
|
return robot.Command{}, errors.New("rejected:raw-xml-root")
|
|
}
|
|
|
|
// Capture inner XML
|
|
var innerBuf bytes.Buffer
|
|
enc := xml.NewEncoder(&innerBuf)
|
|
|
|
var idAttr string
|
|
var tdAttr string
|
|
|
|
for _, attr := range start.Attr {
|
|
if attr.Name.Local == "id" {
|
|
idAttr = attr.Value
|
|
} else if attr.Name.Local == "td" {
|
|
tdAttr = attr.Value
|
|
}
|
|
}
|
|
|
|
if idAttr == "" && tdAttr != "Move" {
|
|
return robot.Command{}, errors.New("rejected:raw-id")
|
|
}
|
|
|
|
for {
|
|
t, err := dec.Token()
|
|
if err != nil {
|
|
break
|
|
}
|
|
if end, ok := t.(xml.EndElement); ok && end.Name.Local == "ctl" {
|
|
break
|
|
}
|
|
if err := enc.EncodeToken(t); err != nil {
|
|
return robot.Command{}, errors.New("rejected:raw-xml-inner")
|
|
}
|
|
}
|
|
enc.Flush()
|
|
|
|
return robot.Command{
|
|
Name: "raw",
|
|
Args: map[string]string{
|
|
"td": tdAttr,
|
|
"id": idAttr,
|
|
},
|
|
Payload: innerBuf.Bytes(),
|
|
}, nil
|
|
|
|
default:
|
|
return robot.Command{}, errors.New("rejected:" + req.Command)
|
|
}
|
|
}
|
|
}
|