新增机器人功能
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package mqtpp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protowire"
|
||||
)
|
||||
|
||||
const NodeNumBroadcast uint32 = 0xffffffff
|
||||
|
||||
type TextMessageBuildOptions struct {
|
||||
FromNodeNum uint32
|
||||
ToNodeNum uint32
|
||||
PacketID uint32
|
||||
ChannelID string
|
||||
GatewayID string
|
||||
Text string
|
||||
PSK []byte
|
||||
Encrypt bool
|
||||
ViaMQTT bool
|
||||
}
|
||||
|
||||
func BuildTextMessageServiceEnvelope(opts TextMessageBuildOptions) ([]byte, error) {
|
||||
if opts.FromNodeNum == 0 {
|
||||
return nil, fmt.Errorf("from node number is required")
|
||||
}
|
||||
if opts.PacketID == 0 {
|
||||
return nil, fmt.Errorf("packet id is required")
|
||||
}
|
||||
if opts.ChannelID == "" {
|
||||
return nil, fmt.Errorf("channel id is required")
|
||||
}
|
||||
if strings.TrimSpace(opts.GatewayID) == "" {
|
||||
opts.GatewayID = NodeNumToID(opts.FromNodeNum)
|
||||
}
|
||||
if opts.Text == "" {
|
||||
return nil, fmt.Errorf("text is required")
|
||||
}
|
||||
if !utf8.ValidString(opts.Text) {
|
||||
return nil, fmt.Errorf("text must be valid utf-8")
|
||||
}
|
||||
|
||||
data := buildDataPacket(textMessageApp, []byte(opts.Text))
|
||||
packet, err := buildMeshPacket(opts, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildServiceEnvelope(packet, opts.ChannelID, opts.GatewayID), nil
|
||||
}
|
||||
|
||||
func NodeNumToID(nodeNum uint32) string {
|
||||
return nodeNumToID(nodeNum)
|
||||
}
|
||||
|
||||
func ParseNodeID(nodeID string) (uint32, error) {
|
||||
value := strings.TrimSpace(nodeID)
|
||||
if value == "" {
|
||||
return 0, fmt.Errorf("node id is required")
|
||||
}
|
||||
value = strings.TrimPrefix(value, "!")
|
||||
if len(value) != 8 {
|
||||
return 0, fmt.Errorf("node id must be !xxxxxxxx")
|
||||
}
|
||||
num, err := strconv.ParseUint(value, 16, 32)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid node id: %w", err)
|
||||
}
|
||||
return uint32(num), nil
|
||||
}
|
||||
|
||||
func buildDataPacket(portnum uint32, payload []byte) []byte {
|
||||
var out []byte
|
||||
out = protowire.AppendTag(out, 1, protowire.VarintType)
|
||||
out = protowire.AppendVarint(out, uint64(portnum))
|
||||
out = protowire.AppendTag(out, 2, protowire.BytesType)
|
||||
out = protowire.AppendBytes(out, payload)
|
||||
return out
|
||||
}
|
||||
|
||||
func buildMeshPacket(opts TextMessageBuildOptions, data []byte) ([]byte, error) {
|
||||
var out []byte
|
||||
out = protowire.AppendTag(out, 1, protowire.Fixed32Type)
|
||||
out = protowire.AppendFixed32(out, opts.FromNodeNum)
|
||||
out = protowire.AppendTag(out, 2, protowire.Fixed32Type)
|
||||
out = protowire.AppendFixed32(out, opts.ToNodeNum)
|
||||
|
||||
if opts.Encrypt {
|
||||
if len(opts.PSK) == 0 {
|
||||
return nil, fmt.Errorf("psk is required for encrypted text message")
|
||||
}
|
||||
ciphertext, err := cryptAESCTR(opts.PSK, opts.FromNodeNum, opts.PacketID, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = protowire.AppendTag(out, 3, protowire.VarintType)
|
||||
out = protowire.AppendVarint(out, uint64(channelHash(opts.ChannelID, opts.PSK)))
|
||||
out = protowire.AppendTag(out, 5, protowire.BytesType)
|
||||
out = protowire.AppendBytes(out, ciphertext)
|
||||
} else {
|
||||
out = protowire.AppendTag(out, 4, protowire.BytesType)
|
||||
out = protowire.AppendBytes(out, data)
|
||||
}
|
||||
|
||||
out = protowire.AppendTag(out, 6, protowire.Fixed32Type)
|
||||
out = protowire.AppendFixed32(out, opts.PacketID)
|
||||
if opts.ViaMQTT {
|
||||
out = protowire.AppendTag(out, 14, protowire.VarintType)
|
||||
out = protowire.AppendVarint(out, 1)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func buildServiceEnvelope(packet []byte, channelID string, gatewayID string) []byte {
|
||||
var out []byte
|
||||
out = protowire.AppendTag(out, 1, protowire.BytesType)
|
||||
out = protowire.AppendBytes(out, packet)
|
||||
out = protowire.AppendTag(out, 2, protowire.BytesType)
|
||||
out = protowire.AppendBytes(out, []byte(channelID))
|
||||
out = protowire.AppendTag(out, 3, protowire.BytesType)
|
||||
out = protowire.AppendBytes(out, []byte(gatewayID))
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package mqtpp
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBuildTextMessageServiceEnvelopeRoundTrip(t *testing.T) {
|
||||
key, err := ExpandPSK("AQ==")
|
||||
if err != nil {
|
||||
t.Fatalf("ExpandPSK() error = %v", err)
|
||||
}
|
||||
|
||||
raw, err := BuildTextMessageServiceEnvelope(TextMessageBuildOptions{
|
||||
FromNodeNum: 0x12345678,
|
||||
ToNodeNum: NodeNumBroadcast,
|
||||
PacketID: 0x87654321,
|
||||
ChannelID: "LongFast",
|
||||
GatewayID: "!12345678",
|
||||
Text: "hello from bot",
|
||||
PSK: key,
|
||||
Encrypt: true,
|
||||
ViaMQTT: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildTextMessageServiceEnvelope() error = %v", err)
|
||||
}
|
||||
|
||||
valid, _, record := MQTTPP("msh/2/e/LongFast/!12345678", raw, key, Options{})
|
||||
if !valid {
|
||||
t.Fatalf("MQTTPP() valid = false, record = %#v", record)
|
||||
}
|
||||
if record["type"] != "text_message" {
|
||||
t.Fatalf("record type = %v", record["type"])
|
||||
}
|
||||
if record["text"] != "hello from bot" {
|
||||
t.Fatalf("text = %v", record["text"])
|
||||
}
|
||||
if record["from_num"] != uint32(0x12345678) {
|
||||
t.Fatalf("from_num = %v", record["from_num"])
|
||||
}
|
||||
if record["packet_to_num"] != uint32(NodeNumBroadcast) {
|
||||
t.Fatalf("packet_to_num = %v", record["packet_to_num"])
|
||||
}
|
||||
if record["decrypt_success"] != true {
|
||||
t.Fatalf("decrypt_success = %v", record["decrypt_success"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTextMessageServiceEnvelopeDirectRoundTrip(t *testing.T) {
|
||||
key, err := ExpandPSK("AQ==")
|
||||
if err != nil {
|
||||
t.Fatalf("ExpandPSK() error = %v", err)
|
||||
}
|
||||
|
||||
raw, err := BuildTextMessageServiceEnvelope(TextMessageBuildOptions{
|
||||
FromNodeNum: 0x12345678,
|
||||
ToNodeNum: 0x10203040,
|
||||
PacketID: 0x11111111,
|
||||
ChannelID: "LongFast",
|
||||
GatewayID: "!12345678",
|
||||
Text: "direct hello",
|
||||
PSK: key,
|
||||
Encrypt: true,
|
||||
ViaMQTT: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildTextMessageServiceEnvelope() error = %v", err)
|
||||
}
|
||||
|
||||
valid, _, record := MQTTPP("msh/2/e/LongFast/!12345678", raw, key, Options{})
|
||||
if !valid {
|
||||
t.Fatalf("MQTTPP() valid = false, record = %#v", record)
|
||||
}
|
||||
if record["text"] != "direct hello" {
|
||||
t.Fatalf("text = %v", record["text"])
|
||||
}
|
||||
if record["packet_to"] != "!10203040" {
|
||||
t.Fatalf("packet_to = %v", record["packet_to"])
|
||||
}
|
||||
if record["packet_to_num"] != uint32(0x10203040) {
|
||||
t.Fatalf("packet_to_num = %v", record["packet_to_num"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNodeID(t *testing.T) {
|
||||
num, err := ParseNodeID("!1234abcd")
|
||||
if err != nil {
|
||||
t.Fatalf("ParseNodeID() error = %v", err)
|
||||
}
|
||||
if num != 0x1234abcd {
|
||||
t.Fatalf("num = %#x", num)
|
||||
}
|
||||
if NodeNumToID(num) != "!1234abcd" {
|
||||
t.Fatalf("NodeNumToID() = %s", NodeNumToID(num))
|
||||
}
|
||||
}
|
||||
+8
-3
@@ -944,6 +944,11 @@ func channelHash(channelName string, key []byte) byte {
|
||||
|
||||
// decryptAESCTR 按 Meshtastic nonce 规则使用 AES-CTR 解密 payload。
|
||||
func decryptAESCTR(key []byte, fromNum, packetID uint32, ciphertext []byte) ([]byte, error) {
|
||||
return cryptAESCTR(key, fromNum, packetID, ciphertext)
|
||||
}
|
||||
|
||||
// cryptAESCTR 按 Meshtastic nonce 规则执行 AES-CTR;CTR 加密和解密是同一个 XOR 流操作。
|
||||
func cryptAESCTR(key []byte, fromNum, packetID uint32, input []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -951,9 +956,9 @@ func decryptAESCTR(key []byte, fromNum, packetID uint32, ciphertext []byte) ([]b
|
||||
nonce := make([]byte, aes.BlockSize)
|
||||
binary.LittleEndian.PutUint64(nonce[0:8], uint64(packetID))
|
||||
binary.LittleEndian.PutUint32(nonce[8:12], fromNum)
|
||||
plaintext := make([]byte, len(ciphertext))
|
||||
cipher.NewCTR(block, nonce).XORKeyStream(plaintext, ciphertext)
|
||||
return plaintext, nil
|
||||
output := make([]byte, len(input))
|
||||
cipher.NewCTR(block, nonce).XORKeyStream(output, input)
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// enumName 把已知枚举值转换成名称,未知值保留为数字。
|
||||
|
||||
Reference in New Issue
Block a user