Quellcode durchsuchen

编写sshd模块代码, 完成服务端代码编写

niujiuru vor 1 Woche
Ursprung
Commit
399a6cb72e
6 geänderte Dateien mit 306 neuen und 9 gelöschten Zeilen
  1. 3 0
      go.mod
  2. 6 0
      go.sum
  3. 11 6
      main.go
  4. 5 3
      netmgrd/modem.go
  5. 35 0
      sshd/protocol.go
  6. 246 0
      sshd/sshd.go

+ 3 - 0
go.mod

@@ -5,6 +5,7 @@ go 1.24.2
 require (
 	github.com/alexflint/go-filemutex v1.3.0
 	github.com/beevik/ntp v1.5.0
+	github.com/eclipse/paho.mqtt.golang v1.5.1
 	github.com/jlaffaye/ftp v0.2.0
 	github.com/mattn/go-shellwords v1.0.12
 	github.com/sirupsen/logrus v1.9.3
@@ -14,10 +15,12 @@ require (
 )
 
 require (
+	github.com/gorilla/websocket v1.5.3 // indirect
 	github.com/hashicorp/errwrap v1.0.0 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
 	github.com/vishvananda/netns v0.0.5 // indirect
 	golang.org/x/net v0.44.0 // indirect
+	golang.org/x/sync v0.17.0 // indirect
 	golang.org/x/sys v0.36.0 // indirect
 )
 

+ 6 - 0
go.sum

@@ -5,6 +5,10 @@ github.com/beevik/ntp v1.5.0/go.mod h1:mJEhBrwT76w9D+IfOEGvuzyuudiW9E52U2BaTrMOY
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
+github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
@@ -28,6 +32,8 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
 github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
 golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
 golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
+golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
+golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

+ 11 - 6
main.go

@@ -8,6 +8,7 @@ import (
 	baseapp "hnyfkj.com.cn/rtu/linux/baseapp"
 	camera1 "hnyfkj.com.cn/rtu/linux/mvs_u_takephoto" // 海康U口相机
 	netmgrd "hnyfkj.com.cn/rtu/linux/netmgrd"
+	"hnyfkj.com.cn/rtu/linux/sshd"
 )
 
 func main() {
@@ -36,22 +37,26 @@ func main() {
 
 	// 03, 初始化网络管理模块
 	netmgrd.ModuleInit()
-	netmgrd.WaitAllOK(time.Duration(10) * time.Second) // 等待联网成功和NTP时间同步成功
+	netmgrd.WaitAllOK(time.Duration(10) * time.Second) // 等待联网成功和网路时间同步成功
 
-	// 04, 初始化相机拍照模块
+	// 04, 初始化远程运维模块, Todo: 根据项目不同, 远程运维连接的MQTT服务器不同, 具体看情况
+	sshd.ModuleInit("", "", "")
+
+	// 05, 初始化相机拍照模块
 	if !camera1.ModuleInit() {
 		goto end_p
 	}
 
-	// 05, 初始化与控制板通信, Todo: 根据项目不同, 使用的控制版硬件和协议都不同, 具体看情况
+	// 06, 初始化与控制板通信, Todo: 根据项目不同, 使用的控制版硬件和协议都不同, 具体看情况
 
-	// 06, 后台服务器业务交互, Todo: 根据项目不同, 实现具体的业务逻辑协议都不同, 具体看情况
+	// 07, 后台服务器业务交互, Todo: 根据项目不同, 实现具体的业务逻辑协议都不同, 具体看情况
 
-	// 07, 阻塞等待退出信号量
+	// 08, 阻塞等待退出信号量
 	<-baseapp.IsExit2()
 
-	// 08, 退出程序并释放资源
+	// 09, 退出程序并释放资源
 end_p:
+	sshd.ModuleExit()
 	netmgrd.ModemExit()
 	gps.ModuleExit()
 

+ 5 - 3
netmgrd/modem.go

@@ -17,6 +17,8 @@ var (
 	curModemType ModemType
 )
 
+const ErrUnknownModemTypeMsg = "未知的调制解调器类型" // ?
+
 func (m ModemType) String() string {
 	switch m {
 	case Air720U:
@@ -66,7 +68,7 @@ func GetIMEI() string {
 	case EC200U:
 		return modem2.GetIMEI()
 	default:
-		return "未知的调制解调器类型"
+		return ErrUnknownModemTypeMsg
 	}
 }
 
@@ -77,7 +79,7 @@ func GetRSSI() string {
 	case EC200U:
 		return modem2.GetRSSI()
 	default:
-		return "未知的调制解调器类型"
+		return ErrUnknownModemTypeMsg
 	}
 }
 
@@ -88,6 +90,6 @@ func GetSimICCID() string {
 	case EC200U:
 		return modem2.GetSimICCID()
 	default:
-		return "未知的调制解调器类型"
+		return ErrUnknownModemTypeMsg
 	}
 }

+ 35 - 0
sshd/protocol.go

@@ -0,0 +1,35 @@
+package sshd
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"hnyfkj.com.cn/rtu/linux/utils/jsonrpc2"
+	"hnyfkj.com.cn/rtu/linux/utils/shell"
+)
+
+func buildResp(req *jsonrpc2.Request, result any, err error) *jsonrpc2.Response {
+	if err != nil {
+		return jsonrpc2.BuildError(req, -32700, err.Error())
+	}
+
+	resp, err := jsonrpc2.BuildResult(req, result)
+	if err != nil {
+		return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, "")
+	}
+
+	return resp
+}
+
+func parseShellExecuteParams(params json.RawMessage) (shell.ExecuteParams, error) {
+	if len(params) == 0 {
+		return shell.ExecuteParams{}, fmt.Errorf("missing params")
+	}
+
+	var p shell.ExecuteParams
+	if err := json.Unmarshal(params, &p); err != nil {
+		return shell.ExecuteParams{}, err
+	}
+
+	return p, nil
+}

+ 246 - 0
sshd/sshd.go

@@ -1 +1,247 @@
 package sshd
+
+import (
+	"context"
+	"errors"
+	"strings"
+	"sync/atomic"
+	"time"
+
+	mqtt "github.com/eclipse/paho.mqtt.golang"
+	"hnyfkj.com.cn/rtu/linux/baseapp"
+	"hnyfkj.com.cn/rtu/linux/netmgrd"
+	"hnyfkj.com.cn/rtu/linux/utils/jsonrpc2"
+	"hnyfkj.com.cn/rtu/linux/utils/shell"
+	"hnyfkj.com.cn/rtu/linux/utils/singletask"
+)
+
+const MODULE_NAME = "sshd_over_mqtt"
+
+var (
+	Coupler *MQTTCoupler
+)
+
+const (
+	MqttQos1     byte = 1               //// 消息至少送达一次
+	FastInterval      = 1 * time.Second //// 快速检测时间间隔
+	SlowInterval      = 5 * time.Second //// 慢速检测时间间隔
+)
+
+var (
+	ErrBrokerAddressEmpty = errors.New("mqtt server address is empty")
+	ErrIMEINotAvailable   = errors.New("device imei is not available")
+)
+
+type MQTTCoupler struct {
+	broker, username, password string
+	client                     mqtt.Client
+
+	imei     string // 设备唯一标识
+	subTopic string // 订阅指令主题:/yfkj/device/rpc/imei/cmd
+	pubTopic string // 发布应答主题:/yfkj/device/rpc/imei/ack
+
+	ctx    context.Context
+	cancel context.CancelFunc
+
+	isConnected atomic.Bool /// 标记是否已连接MQTT的Broker服务
+
+	// 注册本地的远程方法, 连接成功后用于让客户端能够主动下发指令
+	registerRpcMeths *singletask.OnceTask // 注册方法, 单实例
+}
+
+func ModuleInit(mqttBroker, mqttUsername, mqttPassword string) error {
+	if mqttBroker == "" {
+		return ErrBrokerAddressEmpty
+	}
+
+	ctx, cancel := context.WithCancel(context.Background())
+
+	Coupler = &MQTTCoupler{
+		broker:           mqttBroker,
+		username:         mqttUsername,
+		password:         mqttPassword,
+		client:           nil,
+		imei:             "",
+		subTopic:         "",
+		pubTopic:         "",
+		ctx:              ctx,
+		cancel:           cancel,
+		isConnected:      atomic.Bool{},
+		registerRpcMeths: &singletask.OnceTask{},
+	}
+
+	if err := Coupler.init(); err != nil {
+		return err
+	}
+	go Coupler.keepOnline()
+
+	return nil
+}
+
+func ModuleExit() {
+	if Coupler != nil {
+		Coupler.cancel()
+	}
+}
+
+func (c *MQTTCoupler) init() error {
+	c.imei = netmgrd.GetIMEI()
+	if c.imei == netmgrd.ErrUnknownModemTypeMsg || c.imei == "" {
+		return ErrIMEINotAvailable
+	}
+
+	template := "/yfkj/device/rpc/imei"
+	c.subTopic = strings.ReplaceAll(template+"/cmd", "imei", c.imei)
+	c.pubTopic = strings.ReplaceAll(template+"/ack", "imei", c.imei)
+
+	opts := mqtt.NewClientOptions().
+		AddBroker(c.broker).
+		SetUsername(c.username).SetPassword(c.password).
+		SetConnectRetry(false).SetAutoReconnect(false).SetCleanSession(true).
+		SetKeepAlive(10*time.Second).SetPingTimeout(5*time.Second). // Ping心跳间隔, 超时时间
+		SetOrderMatters(false). /*离线遗愿消息*/ SetWill(c.pubTopic, string(`{"jsonrpc": "2.0", "method": "logout"}`), MqttQos1, false)
+
+	opts.OnConnect = func(client mqtt.Client) {
+		if !c.isConnected.Swap(true) {
+			baseapp.Logger.Infof("[%s] MQTT Broker连接成功", MODULE_NAME)
+		}
+	}
+
+	opts.OnConnectionLost = func(client mqtt.Client, err error) {
+		if c.isConnected.Swap(false) {
+			baseapp.Logger.Warnf("[%s] MQTT Broker连接丢失: %v!", MODULE_NAME, err)
+		}
+	}
+
+	c.client = mqtt.NewClient(opts)
+
+	return nil
+}
+
+func (c *MQTTCoupler) keepOnline() {
+	t := time.NewTimer(FastInterval)
+	defer t.Stop()
+
+	for {
+		select {
+		case <-c.ctx.Done():
+			return
+		case <-t.C:
+			t.Reset(c.tick())
+		} // end select
+	} // end for
+}
+
+func (c *MQTTCoupler) tick() time.Duration {
+	if c.isConnected.Load() {
+		return FastInterval
+	}
+
+	if err := c.connect(); err != nil {
+		baseapp.Logger.Errorf("[%s] MQTT Broker连接失败: %v!!", MODULE_NAME, err)
+	} else { // 注册本地的RPC方法, 供远端调用, 单实例运行
+		c.registerRpcMeths.Run(c.instRPCMethods, true)
+	}
+
+	return SlowInterval
+}
+
+func (c *MQTTCoupler) connect() error {
+	if c.client.IsConnected() {
+		return nil
+	}
+
+	token := c.client.Connect()
+	select {
+	case <-c.ctx.Done():
+		return nil
+	case <-token.Done():
+	}
+
+	return token.Error()
+}
+
+func (c *MQTTCoupler) instRPCMethods() {
+	t := time.NewTicker(time.Second)
+	defer t.Stop()
+
+	for {
+		if !c.isConnected.Load() || c.ctx.Err() != nil {
+			return
+		}
+
+		token := c.client.Subscribe(c.subTopic, MqttQos1, c.handleRequests)
+		select {
+		case <-c.ctx.Done():
+			return
+		case <-token.Done():
+		}
+
+		if token.Error() == nil {
+			baseapp.Logger.Infof("[%s] 本地RPC方法已注册, 等待远端调用...", MODULE_NAME)
+			break
+		}
+
+		select {
+		case <-c.ctx.Done():
+			return
+		case <-t.C:
+			continue
+		}
+	}
+}
+
+func (c *MQTTCoupler) handleRequests(client mqtt.Client, msg mqtt.Message) {
+	go c.execOneCmd(msg)
+}
+
+func (c *MQTTCoupler) execOneCmd(msg mqtt.Message) {
+	str := string(msg.Payload())
+	baseapp.Logger.Infof("[%s] 收到一个RPC请求: %s", MODULE_NAME, str)
+
+	var resp *jsonrpc2.Response // 预定义一个空的应答
+
+	req, err := jsonrpc2.ParseRequest(str)
+	if err != nil || req.ID == nil /* 不接受通知类型的消息 */ {
+		resp = jsonrpc2.BuildError(nil, jsonrpc2.ErrParse, "")
+		goto retp
+	}
+
+	switch req.Method {
+	// Call-1: 心跳, 链路检测,"ping-pong"测试
+	case "ping":
+		resp = buildResp(req, "pong", nil)
+	// Call-2:在本地shell中执行远程下发的指令
+	case "shell.execute":
+		params, err := parseShellExecuteParams(req.Params)
+		if err != nil {
+			resp = jsonrpc2.BuildError(req, -32700, err.Error())
+			goto retp
+		}
+		result, err := shell.Execute(params)
+		resp = buildResp(req, result, err)
+	// Call-?:无效, 远端调用了还不支持的-方法
+	default:
+		resp = jsonrpc2.BuildError(req, jsonrpc2.ErrMethodNotFound, "")
+	}
+
+retp:
+	text, err := resp.String()
+	if err != nil {
+		baseapp.Logger.Errorf("[%s] 转换RPC应答失败: %v!!", MODULE_NAME, err)
+		return
+	}
+
+	token := c.client.Publish(c.pubTopic, MqttQos1, false, text)
+	select {
+	case <-c.ctx.Done():
+		return
+	case <-token.Done():
+	}
+
+	if err := token.Error(); err != nil {
+		baseapp.Logger.Errorf("[%s] 发送RPC应答失败: %v!!", MODULE_NAME, err)
+	}
+
+	baseapp.Logger.Infof("[%s] 发送一个RPC应答, 报文内容: %s", MODULE_NAME, text)
+}