mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-09-27 03:25:56 +08:00
1405 lines
40 KiB
Go
1405 lines
40 KiB
Go
package plugin_gb28181pro
|
||
|
||
import (
|
||
"bytes"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/langhuihui/gotask"
|
||
"golang.org/x/text/encoding/simplifiedchinese"
|
||
"golang.org/x/text/transform"
|
||
"m7s.live/v5"
|
||
"m7s.live/v5/pkg/util"
|
||
|
||
"github.com/emiago/sipgo"
|
||
"github.com/emiago/sipgo/sip"
|
||
"github.com/icholy/digest"
|
||
gb28181 "m7s.live/v5/plugin/gb28181/pkg"
|
||
)
|
||
|
||
// Platform 表示GB28181平台的运行时实例
|
||
type Platform struct {
|
||
task.Job `gorm:"-:all"` // 使用TickTask,并且排除 gorm 序列化
|
||
PlatformModel *gb28181.PlatformModel
|
||
|
||
// SIP相关字段,不存储到数据库
|
||
Client *sipgo.Client `gorm:"-" json:"-"` // SIP客户端
|
||
DialogClient *sipgo.DialogClientCache `gorm:"-" json:"-"` // SIP对话客户端
|
||
Recipient sip.Uri `gorm:"-" json:"-"` // 接收者地址
|
||
ContactHDR *sip.ContactHeader `gorm:"-" json:"-"` // 联系人头部
|
||
UserAgentHDR sip.Header `gorm:"-" json:"-"` //
|
||
MaxForwardsHDR sip.MaxForwardsHeader `gorm:"-" json:"-"`
|
||
|
||
// 运行时字段
|
||
KeepAliveReply int `gorm:"-" json:"keepAliveReply"` // KeepAliveReply表示心跳未回复次数
|
||
RegisterCallID string `gorm:"-" json:"registerCallID"` // CallID表示SIP会话的标识符
|
||
SN int
|
||
|
||
// 插件配置
|
||
plugin *GB28181Plugin
|
||
unRegister bool
|
||
channels util.Collection[string, *Channel] `gorm:"-:all"`
|
||
register *Register
|
||
}
|
||
|
||
// UTF8ToGB2312 将UTF-8编码的字符串转换为GB2312编码
|
||
func UTF8ToGB2312(s string) (string, error) {
|
||
reader := transform.NewReader(bytes.NewReader([]byte(s)), simplifiedchinese.GB18030.NewEncoder())
|
||
d, err := io.ReadAll(reader)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return string(d), nil
|
||
}
|
||
|
||
func NewPlatform(pm *gb28181.PlatformModel, plugin *GB28181Plugin, unRegister bool) *Platform {
|
||
p := &Platform{
|
||
PlatformModel: pm,
|
||
plugin: plugin,
|
||
unRegister: unRegister,
|
||
}
|
||
client, err := sipgo.NewClient(p.plugin.ua, sipgo.WithClientHostname(p.PlatformModel.DeviceIP), sipgo.WithClientPort(p.PlatformModel.DevicePort))
|
||
if err != nil {
|
||
p.Error("failed to create sip client", "err", err)
|
||
}
|
||
p.Client = client
|
||
userAgentHeader := sip.NewHeader("User-Agent", "M7S/"+m7s.Version)
|
||
p.UserAgentHDR = userAgentHeader
|
||
|
||
// 创建注册请求的目标URI,使用上级平台的信息
|
||
recipient := sip.Uri{
|
||
User: p.PlatformModel.ServerGBID,
|
||
Host: p.PlatformModel.ServerIP,
|
||
Port: p.PlatformModel.ServerPort,
|
||
}
|
||
|
||
p.Recipient = recipient
|
||
|
||
// 设置联系人头部,使用本地平台的信息
|
||
contactHdr := sip.ContactHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.DeviceGBID,
|
||
Host: p.PlatformModel.DeviceIP,
|
||
Port: p.PlatformModel.DevicePort,
|
||
},
|
||
}
|
||
p.ContactHDR = &contactHdr
|
||
|
||
// 创建对话客户端
|
||
p.DialogClient = sipgo.NewDialogClientCache(p.Client, *p.ContactHDR)
|
||
|
||
p.MaxForwardsHDR = sip.MaxForwardsHeader(70)
|
||
return p
|
||
}
|
||
|
||
func (p *Platform) Start() error {
|
||
if p.unRegister {
|
||
err := p.Unregister()
|
||
if err != nil {
|
||
p.Error("failed to unregister", "err", err)
|
||
}
|
||
p.unRegister = false
|
||
}
|
||
register := NewRegister(p, "firstRegister")
|
||
register.OnStart(func() {
|
||
register.Tick(nil)
|
||
})
|
||
p.register = register
|
||
p.AddTask(register)
|
||
return nil
|
||
}
|
||
|
||
// getResponse 从事务中获取响应
|
||
func (p *Platform) getResponse(tx sip.ClientTransaction) (*sip.Response, error) {
|
||
select {
|
||
case <-tx.Done():
|
||
return nil, fmt.Errorf("事务已终止")
|
||
case res := <-tx.Responses():
|
||
return res, nil
|
||
}
|
||
}
|
||
|
||
// Keepalive 发送心跳请求到上级平台
|
||
func (p *Platform) Keepalive() (*sipgo.DialogClientSession, error) {
|
||
|
||
req := sip.NewRequest("MESSAGE", p.Recipient)
|
||
req.SetTransport(strings.ToUpper(p.PlatformModel.Transport))
|
||
customCallID := fmt.Sprintf("%s-%d@%s", p.PlatformModel.DeviceGBID, time.Now().Unix(), p.PlatformModel.ServerIP)
|
||
callID := sip.CallIDHeader(customCallID)
|
||
req.AppendHeader(&callID)
|
||
|
||
csqHeader := sip.CSeqHeader{
|
||
SeqNo: uint32(p.SN),
|
||
MethodName: "REGISTER",
|
||
}
|
||
p.SN++
|
||
req.AppendHeader(&csqHeader)
|
||
|
||
// 添加From头部
|
||
fromHeader := sip.FromHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.DeviceGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
Params: sip.NewParams(),
|
||
}
|
||
fromHeader.Params.Add("tag", sip.GenerateTagN(16))
|
||
req.AppendHeader(&fromHeader)
|
||
|
||
// 添加To头部
|
||
toHeader := sip.ToHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.ServerGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
}
|
||
req.AppendHeader(&toHeader)
|
||
|
||
//viaHeader := sip.ViaHeader{
|
||
// ProtocolName: "SIP",
|
||
// ProtocolVersion: "2.0",
|
||
// Transport: p.PlatformModel.Transport,
|
||
// Host: p.PlatformModel.DeviceIP,
|
||
// Port: p.PlatformModel.DevicePort,
|
||
// Params: sip.NewParams(),
|
||
//}
|
||
//viaHeader.Params.Add("branch", sip.GenerateBranchN(16)).Add("rport", "")
|
||
//req.AppendHeader(&viaHeader)
|
||
|
||
req.AppendHeader(&p.MaxForwardsHDR)
|
||
|
||
// 添加Contact头部
|
||
req.AppendHeader(p.ContactHDR)
|
||
|
||
req.AppendHeader(p.UserAgentHDR)
|
||
|
||
// 添加Expires头部,根据是否注销设置不同值
|
||
req.AppendHeader(sip.NewHeader("Expires", fmt.Sprintf("%d", p.PlatformModel.Expires)))
|
||
|
||
contentLengthHeader := sip.ContentLengthHeader(0)
|
||
req.AppendHeader(&contentLengthHeader)
|
||
req.SetBody(gb28181.BuildKeepAliveXML(p.SN, p.PlatformModel.DeviceGBID))
|
||
p.SN++
|
||
tx, err := p.Client.TransactionRequest(p, req)
|
||
if err != nil {
|
||
p.Error("keepalive", "error", err.Error())
|
||
return nil, fmt.Errorf("创建事务失败: %v", err)
|
||
}
|
||
defer tx.Terminate()
|
||
|
||
res, err := p.getResponse(tx)
|
||
if err != nil {
|
||
p.Error("keepalive", "error", err.Error())
|
||
return nil, err
|
||
}
|
||
|
||
if res.StatusCode != 200 {
|
||
p.Error("keepalive", "status", res.StatusCode)
|
||
return nil, fmt.Errorf("心跳失败,状态码: %d", res.StatusCode)
|
||
}
|
||
|
||
p.Info("keepalive", "response", res.String())
|
||
return nil, nil
|
||
}
|
||
|
||
// Register 执行注册或注销流程
|
||
func (p *Platform) Register(isUnregister bool) error {
|
||
// 创建基本的REGISTER请求
|
||
req := sip.NewRequest(sip.REGISTER, p.Recipient)
|
||
|
||
// 设置日志标签
|
||
logTag := "register"
|
||
if isUnregister {
|
||
logTag = "unregister"
|
||
}
|
||
|
||
//callid
|
||
if p.RegisterCallID != "" {
|
||
callID := sip.CallIDHeader(p.RegisterCallID)
|
||
req.AppendHeader(&callID)
|
||
} else {
|
||
customCallID := fmt.Sprintf("%d@%s", time.Now().Unix(), p.PlatformModel.DeviceIP)
|
||
callID := sip.CallIDHeader(customCallID)
|
||
req.AppendHeader(&callID)
|
||
}
|
||
|
||
//cseqheader
|
||
csqHeader := sip.CSeqHeader{
|
||
SeqNo: uint32(p.SN),
|
||
MethodName: "REGISTER",
|
||
}
|
||
p.SN++
|
||
req.AppendHeader(&csqHeader)
|
||
|
||
// 设置From头部,使用本地平台的信息
|
||
fromHdr := sip.FromHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.DeviceGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
Params: sip.NewParams(),
|
||
}
|
||
fromHdr.Params.Add("tag", sip.GenerateTagN(16))
|
||
req.AppendHeader(&fromHdr)
|
||
|
||
// 添加To头部
|
||
toHeader := sip.ToHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.DeviceGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
}
|
||
req.AppendHeader(&toHeader)
|
||
|
||
// 添加Via头部
|
||
//viaHeader := sip.ViaHeader{
|
||
// ProtocolName: "SIP",
|
||
// ProtocolVersion: "2.0",
|
||
// Transport: p.PlatformModel.Transport,
|
||
// Host: p.PlatformModel.DeviceIP,
|
||
// Port: p.PlatformModel.DevicePort,
|
||
// Params: sip.NewParams(),
|
||
//}
|
||
//viaHeader.Params.Add("branch", sip.GenerateBranchN(16)).Add("rport", "")
|
||
//req.AppendHeader(&viaHeader)
|
||
|
||
req.AppendHeader(&p.MaxForwardsHDR)
|
||
|
||
// 添加Contact头部
|
||
req.AppendHeader(p.ContactHDR)
|
||
|
||
req.AppendHeader(p.UserAgentHDR)
|
||
|
||
// 添加Expires头部,根据是否注销设置不同值
|
||
if isUnregister {
|
||
req.AppendHeader(sip.NewHeader("Expires", "0"))
|
||
} else {
|
||
req.AppendHeader(sip.NewHeader("Expires", fmt.Sprintf("%d", p.PlatformModel.Expires)))
|
||
}
|
||
|
||
contentLengthHeader := sip.ContentLengthHeader(0)
|
||
req.AppendHeader(&contentLengthHeader)
|
||
// 设置传输协议
|
||
req.SetTransport(strings.ToUpper(p.PlatformModel.Transport))
|
||
|
||
tx, err := p.Client.TransactionRequest(p, req)
|
||
if err != nil {
|
||
p.plugin.Error(logTag, "error", err.Error())
|
||
return fmt.Errorf("创建事务失败: %v", err)
|
||
}
|
||
defer tx.Terminate()
|
||
|
||
// 获取响应
|
||
res, err := p.getResponse(tx)
|
||
if err != nil {
|
||
p.plugin.Error(logTag, "error", err.Error())
|
||
return err
|
||
}
|
||
|
||
// 处理401未授权响应
|
||
if res.StatusCode == 401 {
|
||
// 获取WWW-Authenticate头部
|
||
wwwAuth := res.GetHeader("WWW-Authenticate")
|
||
if wwwAuth == nil {
|
||
p.plugin.Error(logTag, "error", "no auth challenge")
|
||
return fmt.Errorf("no auth challenge")
|
||
}
|
||
|
||
// 解析认证质询
|
||
chal, err := digest.ParseChallenge(wwwAuth.Value())
|
||
if err != nil {
|
||
p.plugin.Error(logTag, "error", err.Error())
|
||
return err
|
||
}
|
||
|
||
p.plugin.Debug("received auth challenge",
|
||
"realm", chal.Realm,
|
||
"nonce", chal.Nonce,
|
||
"algorithm", chal.Algorithm,
|
||
"qop", chal.QOP)
|
||
|
||
// 生成认证响应
|
||
opts := digest.Options{
|
||
Method: req.Method.String(),
|
||
URI: "sip:" + p.PlatformModel.ServerGBDomain,
|
||
Username: p.PlatformModel.Username,
|
||
Password: p.PlatformModel.Password,
|
||
Cnonce: sip.GenerateTagN(16),
|
||
Count: 1,
|
||
}
|
||
|
||
cred, err := digest.Digest(chal, opts)
|
||
if err != nil {
|
||
p.plugin.Error("calculating digest failed", "error", err.Error())
|
||
return err
|
||
}
|
||
|
||
p.plugin.Debug("calculated response info",
|
||
"username", opts.Username,
|
||
"uri", opts.URI,
|
||
"cnonce", opts.Cnonce,
|
||
"count", opts.Count,
|
||
"response", cred.Response)
|
||
|
||
// 创建新的带认证信息的请求
|
||
newReq := req.Clone()
|
||
newReq.RemoveHeader("Via") // 必须由传输层重新生成
|
||
newReq.AppendHeader(sip.NewHeader("Authorization", cred.String()))
|
||
newReq.CSeq().SeqNo = uint32(p.SN) // 更新CSeq序号
|
||
p.SN++
|
||
|
||
// 发送认证请求
|
||
tx, err = p.Client.TransactionRequest(p, newReq, sipgo.ClientRequestAddVia)
|
||
if err != nil {
|
||
p.plugin.Error(logTag, "error", err.Error())
|
||
return err
|
||
}
|
||
defer tx.Terminate()
|
||
|
||
// 获取认证响应
|
||
res, err = p.getResponse(tx)
|
||
if err != nil {
|
||
p.plugin.Error(logTag, "error", err.Error())
|
||
return err
|
||
}
|
||
}
|
||
|
||
// 检查最终响应状态
|
||
if res.StatusCode != 200 {
|
||
p.plugin.Error(logTag, "status", res.StatusCode)
|
||
return fmt.Errorf("%s失败,状态码: %d", logTag, res.StatusCode)
|
||
}
|
||
|
||
p.plugin.Info(logTag, "status", "success")
|
||
// 根据操作类型设置状态
|
||
p.PlatformModel.Status = !isUnregister
|
||
return nil
|
||
}
|
||
|
||
// Unregister 发送注销请求到上级平台
|
||
func (p *Platform) Unregister() error {
|
||
return p.Register(true)
|
||
}
|
||
|
||
// DoRegister 执行注册流程
|
||
func (p *Platform) DoRegister() error {
|
||
return p.Register(false)
|
||
}
|
||
|
||
// PlatformKeepAliveTask 任务
|
||
type PlatformKeepAliveTask struct {
|
||
task.TickTask
|
||
platform *Platform
|
||
}
|
||
|
||
func (k *PlatformKeepAliveTask) GetTickInterval() time.Duration {
|
||
return time.Second * time.Duration(k.platform.PlatformModel.KeepTimeout)
|
||
}
|
||
|
||
func (k *PlatformKeepAliveTask) Tick(any) {
|
||
if !k.platform.PlatformModel.Enable {
|
||
return
|
||
}
|
||
_, err := k.platform.Keepalive()
|
||
if err != nil {
|
||
k.platform.KeepAliveReply++
|
||
k.Error("keepalive", "error", err.Error())
|
||
if k.platform.KeepAliveReply >= 3 {
|
||
k.platform.PlatformModel.Status = false
|
||
// 重新启动注册任务
|
||
//k.platform.Start()
|
||
platform := k.platform
|
||
platform.KeepAliveReply = 0
|
||
k.Stop(fmt.Errorf("max keepalive retries reached"))
|
||
platform.register.registerType = "firstRegister"
|
||
platform.register.Ticker.Reset(time.Second * time.Duration(platform.PlatformModel.Expires))
|
||
platform.register.Tick(nil)
|
||
}
|
||
} else {
|
||
k.platform.KeepAliveReply = 0
|
||
}
|
||
}
|
||
|
||
// OnMessage 处理来自平台的消息
|
||
func (p *Platform) OnMessage(req *sip.Request, tx sip.ServerTransaction, msg *gb28181.Message) error {
|
||
// 更新平台状态
|
||
p.PlatformModel.UpdateTime = time.Now().Format("2006-01-02 15:04:05")
|
||
|
||
// 根据消息类型处理不同的消息
|
||
switch msg.CmdType {
|
||
case "Catalog":
|
||
// 处理目录请求
|
||
return p.handleCatalog(req, tx, msg)
|
||
case "DeviceControl":
|
||
// 处理设备控制请求
|
||
return p.handleDeviceControl(req, tx, msg)
|
||
case "DeviceInfo":
|
||
// 处理设备信息请求
|
||
return p.handleDeviceInfo(req, tx, msg)
|
||
case "DeviceStatus":
|
||
// 处理设备信息请求
|
||
return p.handleDeviceStatus(req, tx, msg)
|
||
case "PresetQuery":
|
||
// 处理预置位查询请求
|
||
return p.handlePresetQuery(req, tx, msg)
|
||
case "Alarm":
|
||
// 处理报警消息
|
||
return p.handleAlarm(req, tx, msg)
|
||
case "MobilePosition":
|
||
// 处理移动位置信息
|
||
return p.handleMobilePosition(req, tx, msg)
|
||
default:
|
||
// 不支持的消息类型,返回错误
|
||
response := sip.NewResponseFromRequest(req, sip.StatusUnsupportedMediaType, "Unsupported message type", nil)
|
||
if err := tx.Respond(response); err != nil {
|
||
return fmt.Errorf("respond error: %v", err)
|
||
}
|
||
return fmt.Errorf("unsupported message type: %s", msg.CmdType)
|
||
}
|
||
}
|
||
|
||
// handleCatalog 处理目录请求
|
||
func (p *Platform) handleCatalog(req *sip.Request, tx sip.ServerTransaction, msg *gb28181.Message) error {
|
||
// 回复 200 OK
|
||
err := tx.Respond(sip.NewResponseFromRequest(req, http.StatusOK, "OK", nil))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 获取 SN 和 FromTag
|
||
sn := strconv.Itoa(msg.SN)
|
||
fromTag, _ := req.From().Params.Get("tag")
|
||
p.plugin.Info("catalog", "sn", sn, "fromTag", fromTag)
|
||
|
||
// 打印平台ID
|
||
p.plugin.Info("catalog query platform_id", "platform_id", p.PlatformModel.ServerGBID)
|
||
|
||
// 查询通道列表
|
||
var channels []gb28181.DeviceChannel
|
||
//if p.plugin.DB != nil {
|
||
// if err := p.plugin.DB.Table("gb28181_channel gc").
|
||
// Select(`gc.*`).
|
||
// Joins("left join gb28181_platform_channel gpc on gc.id=gpc.channel_db_id").
|
||
// Where("gpc.platform_server_gb_id = ? and gc.status='ON'", p.PlatformModel.ServerGBID).
|
||
// Find(&channels).Error; err != nil {
|
||
// return fmt.Errorf("query channels error: %v", err)
|
||
// }
|
||
//}
|
||
for channel := range p.channels.Range {
|
||
channels = append(channels, *channel.DeviceChannel)
|
||
}
|
||
|
||
// 发送目录响应,无论是否有通道
|
||
p.plugin.Info("get channels success", "channels", channels)
|
||
return p.sendCatalogResponse(req, sn, fromTag, channels)
|
||
}
|
||
|
||
// CreateRequest 创建 SIP 请求
|
||
func (p *Platform) CreateRequest(method string) *sip.Request {
|
||
request := sip.NewRequest(sip.RequestMethod(method), p.Recipient)
|
||
//request.SetDestination(p.Recipient.String())
|
||
return request
|
||
}
|
||
|
||
// sendCatalogResponse 发送目录响应
|
||
func (p *Platform) sendCatalogResponse(req *sip.Request, sn string, fromTag string, channels []gb28181.DeviceChannel) error {
|
||
// 如果没有通道,发送一个空的目录列表
|
||
if len(channels) == 0 {
|
||
request := p.CreateRequest("MESSAGE")
|
||
|
||
// 设置From头部
|
||
fromHeader := sip.FromHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.DeviceGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
Params: sip.NewParams(),
|
||
}
|
||
fromHeader.Params.Add("tag", fromTag)
|
||
request.AppendHeader(&fromHeader)
|
||
|
||
// 添加To头部
|
||
toHeader := sip.ToHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.ServerGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
}
|
||
request.AppendHeader(&toHeader)
|
||
|
||
// 添加Via头部
|
||
//viaHeader := sip.ViaHeader{
|
||
// ProtocolName: "SIP",
|
||
// ProtocolVersion: "2.0",
|
||
// Transport: p.PlatformModel.Transport,
|
||
// Host: p.PlatformModel.DeviceIP,
|
||
// Port: p.PlatformModel.DevicePort,
|
||
// Params: sip.NewParams(),
|
||
//}
|
||
//viaHeader.Params.Add("branch", sip.GenerateBranchN(16)).Add("rport", "")
|
||
//request.AppendHeader(&viaHeader)
|
||
|
||
request.SetTransport(req.Transport())
|
||
contentTypeHeader := sip.ContentTypeHeader("Application/MANSCDP+xml")
|
||
request.AppendHeader(&contentTypeHeader)
|
||
|
||
// 空目录列表XML
|
||
xmlContent := fmt.Sprintf(`<?xml version="1.0" encoding="GB2312"?>
|
||
<Response>
|
||
<CmdType>Catalog</CmdType>
|
||
<SN>%s</SN>
|
||
<DeviceID>%s</DeviceID>
|
||
<SumNum>0</SumNum>
|
||
<DeviceList Num="0">
|
||
</DeviceList>
|
||
</Response>`, sn, p.PlatformModel.DeviceGBID)
|
||
request.SetBody([]byte(xmlContent))
|
||
|
||
// 修正:使用TransactionRequest替代Do
|
||
tx, err := p.Client.TransactionRequest(p, request)
|
||
if err != nil {
|
||
p.Error("sendCatalogResponse", "error", err.Error())
|
||
return fmt.Errorf("创建事务失败: %v", err)
|
||
}
|
||
defer tx.Terminate()
|
||
|
||
// 获取响应
|
||
res, err := p.getResponse(tx)
|
||
if err != nil {
|
||
p.Error("sendCatalogResponse", "error", err.Error())
|
||
return err
|
||
}
|
||
|
||
// 处理401未授权响应
|
||
if res.StatusCode == 401 {
|
||
// 获取WWW-Authenticate头部
|
||
wwwAuth := res.GetHeader("WWW-Authenticate")
|
||
if wwwAuth == nil {
|
||
p.Error("sendCatalogResponse", "error", "no auth challenge")
|
||
return fmt.Errorf("no auth challenge")
|
||
}
|
||
|
||
// 解析认证质询
|
||
chal, err := digest.ParseChallenge(wwwAuth.Value())
|
||
if err != nil {
|
||
p.Error("sendCatalogResponse", "error", err.Error())
|
||
return err
|
||
}
|
||
|
||
p.plugin.Debug("received auth challenge",
|
||
"realm", chal.Realm,
|
||
"nonce", chal.Nonce,
|
||
"algorithm", chal.Algorithm,
|
||
"qop", chal.QOP)
|
||
|
||
// 生成认证响应
|
||
opts := digest.Options{
|
||
Method: request.Method.String(),
|
||
URI: "sip:" + p.PlatformModel.ServerGBDomain,
|
||
Username: p.PlatformModel.Username,
|
||
Password: p.PlatformModel.Password,
|
||
Cnonce: sip.GenerateTagN(16),
|
||
Count: 1,
|
||
}
|
||
|
||
cred, err := digest.Digest(chal, opts)
|
||
if err != nil {
|
||
p.Error("calculating digest failed", "error", err.Error())
|
||
return err
|
||
}
|
||
|
||
p.plugin.Debug("calculated response info",
|
||
"username", opts.Username,
|
||
"uri", opts.URI,
|
||
"cnonce", opts.Cnonce,
|
||
"count", opts.Count,
|
||
"response", cred.Response)
|
||
|
||
// 创建新的带认证信息的请求
|
||
newReq := request.Clone()
|
||
newReq.RemoveHeader("Via") // 必须由传输层重新生成
|
||
newReq.AppendHeader(sip.NewHeader("Authorization", cred.String()))
|
||
|
||
// 发送认证请求
|
||
tx, err = p.Client.TransactionRequest(p, newReq, sipgo.ClientRequestAddVia)
|
||
if err != nil {
|
||
p.Error("sendCatalogResponse", "error", err.Error())
|
||
return err
|
||
}
|
||
defer tx.Terminate()
|
||
|
||
// 获取认证响应
|
||
res, err = p.getResponse(tx)
|
||
if err != nil {
|
||
p.Error("sendCatalogResponse", "error", err.Error())
|
||
return err
|
||
}
|
||
}
|
||
|
||
// 检查最终响应状态
|
||
if res.StatusCode != 200 {
|
||
p.Error("sendCatalogResponse", "status", res.StatusCode)
|
||
return fmt.Errorf("发送目录响应失败,状态码: %d", res.StatusCode)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// 有通道时,为每个通道单独发送一个XML
|
||
for i, channel := range channels {
|
||
request := p.CreateRequest("MESSAGE")
|
||
|
||
// 设置From头部
|
||
fromHeader := sip.FromHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.DeviceGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
Params: sip.NewParams(),
|
||
}
|
||
fromHeader.Params.Add("tag", fromTag)
|
||
request.AppendHeader(&fromHeader)
|
||
|
||
// 添加To头部
|
||
toHeader := sip.ToHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.ServerGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
}
|
||
request.AppendHeader(&toHeader)
|
||
|
||
// 添加Via头部
|
||
//viaHeader := sip.ViaHeader{
|
||
// ProtocolName: "SIP",
|
||
// ProtocolVersion: "2.0",
|
||
// Transport: p.PlatformModel.Transport,
|
||
// Host: p.PlatformModel.DeviceIP,
|
||
// Port: p.PlatformModel.DevicePort,
|
||
// Params: sip.NewParams(),
|
||
//}
|
||
//viaHeader.Params.Add("branch", sip.GenerateBranchN(16)).Add("rport", "")
|
||
//request.AppendHeader(&viaHeader)
|
||
|
||
request.SetTransport(req.Transport())
|
||
contentTypeHeader := sip.ContentTypeHeader("Application/MANSCDP+xml")
|
||
request.AppendHeader(&contentTypeHeader)
|
||
|
||
// 为单个通道创建XML
|
||
channelXML := p.buildChannelItem(channel)
|
||
xmlContent := fmt.Sprintf(`<?xml version="1.0" encoding="GB2312"?>
|
||
<Response>
|
||
<CmdType>Catalog</CmdType>
|
||
<SN>%s</SN>
|
||
<DeviceID>%s</DeviceID>
|
||
<SumNum>%d</SumNum>
|
||
<DeviceList Num="1">
|
||
%s
|
||
</DeviceList>
|
||
</Response>`, sn, p.PlatformModel.DeviceGBID, len(channels), channelXML)
|
||
|
||
request.SetBody([]byte(xmlContent))
|
||
|
||
// 修正:使用TransactionRequest替代Do
|
||
tx, err := p.Client.TransactionRequest(p, request)
|
||
if err != nil {
|
||
p.Error("sendCatalogResponse", "error", err.Error(), "channel_index", i)
|
||
return fmt.Errorf("创建事务失败: %v", err)
|
||
}
|
||
defer tx.Terminate()
|
||
|
||
// 获取响应
|
||
res, err := p.getResponse(tx)
|
||
if err != nil {
|
||
p.Error("sendCatalogResponse", "error", err.Error(), "channel_index", i)
|
||
return err
|
||
}
|
||
|
||
// 处理401未授权响应
|
||
if res.StatusCode == 401 {
|
||
// 获取WWW-Authenticate头部
|
||
wwwAuth := res.GetHeader("WWW-Authenticate")
|
||
if wwwAuth == nil {
|
||
p.Error("sendCatalogResponse", "error", "no auth challenge", "channel_index", i)
|
||
return fmt.Errorf("no auth challenge")
|
||
}
|
||
|
||
// 解析认证质询
|
||
chal, err := digest.ParseChallenge(wwwAuth.Value())
|
||
if err != nil {
|
||
p.Error("sendCatalogResponse", "error", err.Error(), "channel_index", i)
|
||
return err
|
||
}
|
||
|
||
p.plugin.Debug("received auth challenge",
|
||
"realm", chal.Realm,
|
||
"nonce", chal.Nonce,
|
||
"algorithm", chal.Algorithm,
|
||
"qop", chal.QOP,
|
||
"channel_index", i)
|
||
|
||
// 生成认证响应
|
||
opts := digest.Options{
|
||
Method: request.Method.String(),
|
||
URI: "sip:" + p.PlatformModel.ServerGBDomain,
|
||
Username: p.PlatformModel.Username,
|
||
Password: p.PlatformModel.Password,
|
||
Cnonce: sip.GenerateTagN(16),
|
||
Count: 1,
|
||
}
|
||
|
||
cred, err := digest.Digest(chal, opts)
|
||
if err != nil {
|
||
p.Error("calculating digest failed", "error", err.Error(), "channel_index", i)
|
||
return err
|
||
}
|
||
|
||
p.plugin.Debug("calculated response info",
|
||
"username", opts.Username,
|
||
"uri", opts.URI,
|
||
"cnonce", opts.Cnonce,
|
||
"count", opts.Count,
|
||
"response", cred.Response,
|
||
"channel_index", i)
|
||
|
||
// 创建新的带认证信息的请求
|
||
newReq := request.Clone()
|
||
newReq.RemoveHeader("Via") // 必须由传输层重新生成
|
||
newReq.AppendHeader(sip.NewHeader("Authorization", cred.String()))
|
||
|
||
// 发送认证请求
|
||
tx, err = p.Client.TransactionRequest(p, newReq, sipgo.ClientRequestAddVia)
|
||
if err != nil {
|
||
p.Error("sendCatalogResponse", "error", err.Error(), "channel_index", i)
|
||
return err
|
||
}
|
||
defer tx.Terminate()
|
||
|
||
// 获取认证响应
|
||
res, err = p.getResponse(tx)
|
||
if err != nil {
|
||
p.Error("sendCatalogResponse", "error", err.Error(), "channel_index", i)
|
||
return err
|
||
}
|
||
}
|
||
|
||
// 检查最终响应状态
|
||
if res.StatusCode != 200 {
|
||
p.Error("sendCatalogResponse", "status", res.StatusCode, "channel_index", i)
|
||
return fmt.Errorf("发送目录响应失败,状态码: %d", res.StatusCode)
|
||
}
|
||
|
||
// 添加短暂延迟以防止发送过快
|
||
time.Sleep(time.Millisecond * 50)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// buildChannelItem 构建单个通道的XML项
|
||
func (p *Platform) buildChannelItem(channel gb28181.DeviceChannel) string {
|
||
// 确保字符串字段不为空
|
||
deviceID := channel.ChannelId
|
||
if deviceID == "" {
|
||
deviceID = "unknown_device" // 如果没有设备ID,使用默认值
|
||
}
|
||
name := channel.Name
|
||
if name == "" {
|
||
name = "未命名设备"
|
||
}
|
||
manufacturer := channel.Manufacturer
|
||
if manufacturer == "" {
|
||
manufacturer = "未知厂商"
|
||
}
|
||
model := channel.Model
|
||
if model == "" {
|
||
model = "未知型号"
|
||
}
|
||
owner := channel.Owner
|
||
if owner == "" {
|
||
owner = "未知所有者"
|
||
}
|
||
address := channel.Address
|
||
if address == "" {
|
||
address = "未知地址"
|
||
}
|
||
parentID := channel.ParentId
|
||
if parentID == "" {
|
||
parentID = p.PlatformModel.DeviceGBID // 使用平台ID作为父ID
|
||
}
|
||
|
||
return fmt.Sprintf(`<Item>
|
||
<DeviceID>%s</DeviceID>
|
||
<Name>%s</Name>
|
||
<Manufacturer>%s</Manufacturer>
|
||
<Model>%s</Model>
|
||
<Owner>%s</Owner>
|
||
<Address>%s</Address>
|
||
<RegisterWay>%d</RegisterWay>
|
||
<Secrecy>%d</Secrecy>
|
||
<ParentID>%s</ParentID>
|
||
<Parental>%d</Parental>
|
||
<SafetyWay>%d</SafetyWay>
|
||
<Status>ON</Status>
|
||
<Info>
|
||
</Info>
|
||
</Item>`, deviceID, name, manufacturer, model,
|
||
owner, address,
|
||
channel.RegisterWay, // 直接使用整数值
|
||
channel.Secrecy, // 直接使用整数值
|
||
parentID,
|
||
channel.Parental, // 直接使用整数值
|
||
channel.SafetyWay) // 直接使用整数值
|
||
}
|
||
|
||
// handleDeviceControl 处理设备控制请求
|
||
func (p *Platform) handleDeviceControl(req *sip.Request, tx sip.ServerTransaction, msg *gb28181.Message) error {
|
||
// 首先回复200 OK给上级平台
|
||
err := tx.Respond(sip.NewResponseFromRequest(req, http.StatusOK, "OK", nil))
|
||
if err != nil {
|
||
return fmt.Errorf("respond error: %v", err)
|
||
}
|
||
|
||
// 获取通道ID
|
||
channelId := msg.DeviceID
|
||
var deviceId string
|
||
|
||
if tmpChannel, ok := p.plugin.channels.Find(func(c *Channel) bool {
|
||
return c.ChannelId == channelId
|
||
}); ok {
|
||
deviceId = tmpChannel.DeviceId
|
||
} else {
|
||
p.Error("设备不存在或未注册", "device_id", msg.DeviceID)
|
||
return fmt.Errorf("device not found or not registered: %v", msg.DeviceID)
|
||
}
|
||
|
||
// 从devices集合中获取设备实例
|
||
device, ok := p.plugin.devices.Get(deviceId)
|
||
if !ok {
|
||
p.Error("设备不存在或未注册", "device_id", deviceId)
|
||
return fmt.Errorf("device not found or not registered: %v", deviceId)
|
||
}
|
||
|
||
// 创建转发请求
|
||
request := sip.NewRequest(sip.MESSAGE, device.Recipient)
|
||
|
||
// 设置From头部,使用平台信息
|
||
fromHeader := device.fromHDR
|
||
fromTag, _ := req.From().Params.Get("tag")
|
||
fromHeader.Params.Add("tag", fromTag)
|
||
request.AppendHeader(&fromHeader)
|
||
|
||
// 添加To头部,使用设备信息
|
||
toHeader := sip.ToHeader{
|
||
Address: device.Recipient,
|
||
}
|
||
request.AppendHeader(&toHeader)
|
||
|
||
// 添加Via头部
|
||
//viaHeader := sip.ViaHeader{
|
||
// ProtocolName: "SIP",
|
||
// ProtocolVersion: "2.0",
|
||
// Transport: device.Transport,
|
||
// Host: device.SipIp,
|
||
// Port: device.LocalPort,
|
||
// Params: sip.NewParams(),
|
||
//}
|
||
//viaHeader.Params.Add("branch", sip.GenerateBranchN(16)).Add("rport", "")
|
||
//request.AppendHeader(&viaHeader)
|
||
|
||
// 设置Content-Type
|
||
contentTypeHeader := sip.ContentTypeHeader("Application/MANSCDP+xml")
|
||
request.AppendHeader(&contentTypeHeader)
|
||
|
||
// 直接使用原始消息体
|
||
request.SetBody(req.Body())
|
||
|
||
// 设置传输协议
|
||
request.SetTransport(strings.ToUpper(device.Transport))
|
||
|
||
// 发送请求
|
||
_, err = device.client.Do(p, request)
|
||
if err != nil {
|
||
p.Error("发送控制命令失败", "error", err.Error())
|
||
return fmt.Errorf("send control command failed: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// handleDeviceStatus 处理设备状态查询请求
|
||
func (p *Platform) handleDeviceStatus(req *sip.Request, tx sip.ServerTransaction, msg *gb28181.Message) error {
|
||
// 先回复200 OK
|
||
err := tx.Respond(sip.NewResponseFromRequest(req, http.StatusOK, "OK", nil))
|
||
if err != nil {
|
||
return fmt.Errorf("respond error: %v", err)
|
||
}
|
||
|
||
// 获取 SN 和 FromTag
|
||
sn := strconv.Itoa(msg.SN)
|
||
fromTag, _ := req.From().Params.Get("tag")
|
||
|
||
// 获取请求的设备ID
|
||
channelId := msg.DeviceID
|
||
|
||
// 1. 判断是否是查询平台自身信息
|
||
if p.PlatformModel.DeviceGBID == channelId {
|
||
// 如果是查询平台信息,直接返回平台状态
|
||
return p.sendDeviceStatusResponse(req, nil, sn, fromTag)
|
||
}
|
||
|
||
// 2. 查询通道和设备信息
|
||
type Result struct {
|
||
DeviceID string `gorm:"column:device_id"`
|
||
}
|
||
var result Result
|
||
|
||
if p.plugin.DB != nil {
|
||
// 多表联查: channel_gb28181pro -> device_gb28181pro -> devices
|
||
if err := p.plugin.DB.Table("gb28181_channel gc").
|
||
Select("gd.device_id").
|
||
Joins("LEFT JOIN gb28181_device gd ON gc.device_id = gd.device_id").
|
||
Where("gc.channel_id = ?", channelId).
|
||
First(&result).Error; err != nil {
|
||
p.Error("查询通道和设备信息失败", "error", err.Error())
|
||
return fmt.Errorf("channel or device not found: %v", err)
|
||
}
|
||
}
|
||
|
||
// 3. 从devices集合中获取设备实例
|
||
device, ok := p.plugin.devices.Get(result.DeviceID)
|
||
if !ok {
|
||
p.Error("设备不存在或未注册", "device_id", result.DeviceID)
|
||
return fmt.Errorf("device not found or not registered: %v", result.DeviceID)
|
||
}
|
||
|
||
// 4. 发送设备状态响应
|
||
return p.sendDeviceStatusResponse(req, device, sn, fromTag)
|
||
}
|
||
|
||
// sendDeviceStatusResponse 发送设备状态响应
|
||
func (p *Platform) sendDeviceStatusResponse(req *sip.Request, device *Device, sn string, fromTag string) error {
|
||
request := p.CreateRequest("MESSAGE")
|
||
|
||
// 设置From头部
|
||
fromHeader := sip.FromHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.DeviceGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
Params: sip.NewParams(),
|
||
}
|
||
fromHeader.Params.Add("tag", fromTag)
|
||
request.AppendHeader(&fromHeader)
|
||
|
||
// 添加To头部
|
||
toHeader := sip.ToHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.ServerGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
}
|
||
request.AppendHeader(&toHeader)
|
||
|
||
// 添加Via头部
|
||
//viaHeader := sip.ViaHeader{
|
||
// ProtocolName: "SIP",
|
||
// ProtocolVersion: "2.0",
|
||
// Transport: p.PlatformModel.Transport,
|
||
// Host: p.PlatformModel.DeviceIP,
|
||
// Port: p.PlatformModel.DevicePort,
|
||
// Params: sip.NewParams(),
|
||
//}
|
||
//viaHeader.Params.Add("branch", sip.GenerateBranchN(16)).Add("rport", "")
|
||
//request.AppendHeader(&viaHeader)
|
||
|
||
// 设置Content-Type
|
||
contentTypeHeader := sip.ContentTypeHeader("Application/MANSCDP+xml")
|
||
request.AppendHeader(&contentTypeHeader)
|
||
|
||
// 获取当前时间,格式化为设备时间
|
||
currentTime := time.Now().Format("2006-01-02T15:04:05")
|
||
|
||
// 根据设备状态构建响应
|
||
var deviceID, online, status, encode, record string
|
||
if device == nil {
|
||
// 平台自身状态
|
||
deviceID = p.PlatformModel.DeviceGBID
|
||
online = "ONLINE"
|
||
status = "OK"
|
||
encode = "ON"
|
||
record = "OFF"
|
||
} else {
|
||
// 设备状态
|
||
deviceID = device.DeviceId
|
||
// 将布尔值转换为对应的状态字符串
|
||
if device.Online {
|
||
online = "ONLINE"
|
||
status = "OK"
|
||
encode = "ON" // 在线时默认编码开启
|
||
record = "OFF" // 默认不录制
|
||
} else {
|
||
online = "OFFLINE"
|
||
status = "ERROR"
|
||
encode = "OFF" // 离线时编码关闭
|
||
record = "OFF" // 离线时不录制
|
||
}
|
||
}
|
||
|
||
// 构建响应XML
|
||
xmlContent := fmt.Sprintf(`<?xml version="1.0" encoding="GB2312" standalone="yes" ?>
|
||
<Response>
|
||
<CmdType>DeviceStatus</CmdType>
|
||
<SN>%s</SN>
|
||
<DeviceID>%s</DeviceID>
|
||
<Result>OK</Result>
|
||
<Online>%s</Online>
|
||
<Status>%s</Status>
|
||
<DeviceTime>%s</DeviceTime>
|
||
<Encode>%s</Encode>
|
||
<Record>%s</Record>
|
||
<Alarmstatus Num="0"/>
|
||
</Response>`, sn, deviceID, online, status, currentTime, encode, record)
|
||
|
||
request.SetBody([]byte(xmlContent))
|
||
|
||
// 设置传输协议
|
||
request.SetTransport(strings.ToUpper(p.PlatformModel.Transport))
|
||
|
||
// 发送响应
|
||
_, err := p.Client.Do(p, request)
|
||
if err != nil {
|
||
p.Error("发送设备状态响应失败", "error", err.Error())
|
||
return fmt.Errorf("send device status response failed: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// handleDeviceInfo 处理设备信息查询请求
|
||
func (p *Platform) handleDeviceInfo(req *sip.Request, tx sip.ServerTransaction, msg *gb28181.Message) error {
|
||
// 先回复200 OK
|
||
err := tx.Respond(sip.NewResponseFromRequest(req, http.StatusOK, "OK", nil))
|
||
if err != nil {
|
||
return fmt.Errorf("respond error: %v", err)
|
||
}
|
||
|
||
// 获取 SN 和 FromTag
|
||
sn := strconv.Itoa(msg.SN)
|
||
fromTag, _ := req.From().Params.Get("tag")
|
||
|
||
// 获取请求的设备ID
|
||
channelId := msg.DeviceID
|
||
|
||
// 1. 判断是否是查询平台自身信息
|
||
if p.PlatformModel.DeviceGBID == channelId {
|
||
// 如果是查询平台信息,直接返回平台信息
|
||
return p.sendDeviceInfoResponse(req, nil, sn, fromTag)
|
||
}
|
||
|
||
// 2. 查询通道信息
|
||
var channel gb28181.DeviceChannel
|
||
if p.plugin.DB != nil {
|
||
if err := p.plugin.DB.Where("device_id = ? AND channel_id = ?", p.PlatformModel.ServerGBID, channelId).First(&channel).Error; err != nil {
|
||
// 通道不存在,返回404
|
||
response := sip.NewResponseFromRequest(req, sip.StatusNotFound, "channel not found or offline", nil)
|
||
return tx.Respond(response)
|
||
}
|
||
}
|
||
|
||
// 3. 判断通道类型
|
||
if channel.DeviceId == "" {
|
||
// 非国标通道不支持设备信息查询
|
||
response := sip.NewResponseFromRequest(req, sip.StatusForbidden, "non-gb channel not supported", nil)
|
||
return tx.Respond(response)
|
||
}
|
||
|
||
// 4. 查询设备信息
|
||
var device Device
|
||
if p.plugin.DB != nil {
|
||
if err := p.plugin.DB.First(&device, channel.DeviceId).Error; err != nil {
|
||
// 设备不存在,返回404
|
||
response := sip.NewResponseFromRequest(req, sip.StatusNotFound, "device not found", nil)
|
||
return tx.Respond(response)
|
||
}
|
||
}
|
||
|
||
// 5. 发送设备信息响应
|
||
return p.sendDeviceInfoResponse(req, &device, sn, fromTag)
|
||
}
|
||
|
||
// sendDeviceInfoResponse 发送设备信息响应
|
||
func (p *Platform) sendDeviceInfoResponse(req *sip.Request, device *Device, sn string, fromTag string) error {
|
||
request := p.CreateRequest("MESSAGE")
|
||
// 设置From头部
|
||
fromHeader := sip.FromHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.DeviceGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
Params: sip.NewParams(),
|
||
}
|
||
fromHeader.Params.Add("tag", fromTag)
|
||
request.AppendHeader(&fromHeader)
|
||
// 添加To头部
|
||
toHeader := sip.ToHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.ServerGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
}
|
||
request.AppendHeader(&toHeader)
|
||
// 添加Via头部
|
||
//viaHeader := sip.ViaHeader{
|
||
// ProtocolName: "SIP",
|
||
// ProtocolVersion: "2.0",
|
||
// Transport: p.PlatformModel.Transport,
|
||
// Host: p.PlatformModel.DeviceIP,
|
||
// Port: p.PlatformModel.DevicePort,
|
||
// Params: sip.NewParams(),
|
||
//}
|
||
//viaHeader.Params.Add("branch", sip.GenerateBranchN(16)).Add("rport", "")
|
||
//request.AppendHeader(&viaHeader)
|
||
contentTypeHeader := sip.ContentTypeHeader("Application/MANSCDP+xml")
|
||
request.AppendHeader(&contentTypeHeader)
|
||
|
||
// 构建响应XML
|
||
var xmlContent string
|
||
if device == nil {
|
||
// 返回平台信息
|
||
xmlContent = fmt.Sprintf(`<?xml version="1.0" encoding="GB2312"?>
|
||
<Response>
|
||
<CmdType>DeviceInfo</CmdType>
|
||
<SN>%s</SN>
|
||
<DeviceID>%s</DeviceID>
|
||
<Result>OK</Result>
|
||
<DeviceName>%s</DeviceName>
|
||
<Manufacturer>%s</Manufacturer>
|
||
<Model>%s</Model>
|
||
<Firmware>%s</Firmware>
|
||
<Channel>%d</Channel>
|
||
</Response>`, sn, p.PlatformModel.DeviceGBID, p.PlatformModel.Name, p.PlatformModel.Manufacturer, p.PlatformModel.Model, "", p.PlatformModel.ChannelCount)
|
||
} else {
|
||
// 返回设备信息
|
||
xmlContent = fmt.Sprintf(`<?xml version="1.0" encoding="GB2312"?>
|
||
<Response>
|
||
<CmdType>DeviceInfo</CmdType>
|
||
<SN>%s</SN>
|
||
<DeviceID>%s</DeviceID>
|
||
<Result>OK</Result>
|
||
<DeviceName>%s</DeviceName>
|
||
<Manufacturer>%s</Manufacturer>
|
||
<Model>%s</Model>
|
||
<Firmware>%s</Firmware>
|
||
<Channel>%d</Channel>
|
||
</Response>`, sn, device.DeviceId, device.Name, device.Manufacturer, device.Model, device.Firmware, device.ChannelCount)
|
||
}
|
||
|
||
// 将UTF-8编码的XML内容转换为GB2312编码
|
||
gb2312Content, err := UTF8ToGB2312(xmlContent)
|
||
if err != nil {
|
||
p.Error("sendDeviceInfoResponse", "encoding error", err.Error())
|
||
// 如果转换失败,仍然使用原始内容,避免完全失败
|
||
request.SetBody([]byte(xmlContent))
|
||
} else {
|
||
// 使用转换后的GB2312编码内容
|
||
request.SetBody([]byte(gb2312Content))
|
||
}
|
||
|
||
// 修正:使用正确的上下文参数
|
||
tx, err := p.Client.TransactionRequest(p, request)
|
||
if err != nil {
|
||
p.Error("sendDeviceInfoResponse", "error", err.Error())
|
||
return fmt.Errorf("创建事务失败: %v", err)
|
||
}
|
||
defer tx.Terminate()
|
||
|
||
// 获取响应
|
||
res, err := p.getResponse(tx)
|
||
if err != nil {
|
||
p.Error("sendDeviceInfoResponse", "error", err.Error())
|
||
return err
|
||
}
|
||
|
||
// 处理401未授权响应
|
||
if res.StatusCode == 401 {
|
||
// 获取WWW-Authenticate头部
|
||
wwwAuth := res.GetHeader("WWW-Authenticate")
|
||
if wwwAuth == nil {
|
||
p.Error("sendDeviceInfoResponse", "error", "no auth challenge")
|
||
return fmt.Errorf("no auth challenge")
|
||
}
|
||
|
||
// 解析认证质询
|
||
chal, err := digest.ParseChallenge(wwwAuth.Value())
|
||
if err != nil {
|
||
p.Error("sendDeviceInfoResponse", "error", err.Error())
|
||
return err
|
||
}
|
||
|
||
p.Debug("received auth challenge",
|
||
"realm", chal.Realm,
|
||
"nonce", chal.Nonce,
|
||
"algorithm", chal.Algorithm,
|
||
"qop", chal.QOP)
|
||
|
||
// 生成认证响应
|
||
opts := digest.Options{
|
||
Method: request.Method.String(),
|
||
URI: "sip:" + p.PlatformModel.ServerGBDomain,
|
||
Username: p.PlatformModel.Username,
|
||
Password: p.PlatformModel.Password,
|
||
Cnonce: sip.GenerateTagN(16),
|
||
Count: 1,
|
||
}
|
||
|
||
cred, err := digest.Digest(chal, opts)
|
||
if err != nil {
|
||
p.Error("calculating digest failed", "error", err.Error())
|
||
return err
|
||
}
|
||
|
||
p.Debug("calculated response info",
|
||
"username", opts.Username,
|
||
"uri", opts.URI,
|
||
"cnonce", opts.Cnonce,
|
||
"count", opts.Count,
|
||
"response", cred.Response)
|
||
|
||
// 创建新的带认证信息的请求
|
||
newReq := request.Clone()
|
||
newReq.RemoveHeader("Via") // 必须由传输层重新生成
|
||
newReq.AppendHeader(sip.NewHeader("Authorization", cred.String()))
|
||
|
||
// 发送认证请求
|
||
tx, err = p.Client.TransactionRequest(p, newReq, sipgo.ClientRequestAddVia)
|
||
if err != nil {
|
||
p.Error("sendDeviceInfoResponse", "error", err.Error())
|
||
return err
|
||
}
|
||
defer tx.Terminate()
|
||
|
||
// 获取认证响应
|
||
res, err = p.getResponse(tx)
|
||
if err != nil {
|
||
p.Error("sendDeviceInfoResponse", "error", err.Error())
|
||
return err
|
||
}
|
||
}
|
||
|
||
// 检查最终响应状态
|
||
if res.StatusCode != 200 {
|
||
p.Error("sendDeviceInfoResponse", "status", res.StatusCode)
|
||
return fmt.Errorf("发送设备信息响应失败,状态码: %d", res.StatusCode)
|
||
}
|
||
|
||
p.Info("sendDeviceInfoResponse", "status", "success")
|
||
return nil
|
||
}
|
||
|
||
// handleAlarm 处理报警消息
|
||
func (p *Platform) handleAlarm(req *sip.Request, tx sip.ServerTransaction, msg *gb28181.Message) error {
|
||
// TODO: 实现报警消息处理
|
||
response := sip.NewResponseFromRequest(req, sip.StatusOK, "OK", nil)
|
||
return tx.Respond(response)
|
||
}
|
||
|
||
// handleMobilePosition 处理移动位置信息
|
||
func (p *Platform) handleMobilePosition(req *sip.Request, tx sip.ServerTransaction, msg *gb28181.Message) error {
|
||
// TODO: 实现移动位置信息处理
|
||
response := sip.NewResponseFromRequest(req, sip.StatusOK, "OK", nil)
|
||
return tx.Respond(response)
|
||
}
|
||
|
||
// handlePresetQuery 处理预置位查询请求
|
||
func (p *Platform) handlePresetQuery(req *sip.Request, tx sip.ServerTransaction, msg *gb28181.Message) error {
|
||
// 首先回复200 OK给上级平台
|
||
err := tx.Respond(sip.NewResponseFromRequest(req, http.StatusOK, "OK", nil))
|
||
if err != nil {
|
||
return fmt.Errorf("respond error: %v", err)
|
||
}
|
||
|
||
// 获取通道ID
|
||
channelID := msg.DeviceID
|
||
|
||
// 查询通道和设备信息
|
||
type Result struct {
|
||
DeviceID string `gorm:"column:device_id"`
|
||
}
|
||
var result Result
|
||
|
||
if p.plugin.DB != nil {
|
||
// 多表联查: channel_gb28181pro -> device_gb28181pro -> devices
|
||
if err := p.plugin.DB.Table("gb28181_channel gc").
|
||
Select("gc.device_id").
|
||
Joins("LEFT JOIN gb28181_device gd ON gd.device_id = gc.device_id").
|
||
Where("gc.channel_id = ?", channelID).
|
||
First(&result).Error; err != nil {
|
||
p.Error("查询通道和设备信息失败", "error", err.Error())
|
||
return fmt.Errorf("channel or device not found: %v", err)
|
||
}
|
||
}
|
||
|
||
// 从devices集合中获取设备实例
|
||
device, ok := p.plugin.devices.Get(result.DeviceID)
|
||
if !ok {
|
||
p.Error("设备不存在或未注册", "device_id", result.DeviceID)
|
||
return fmt.Errorf("device not found or not registered: %v", result.DeviceID)
|
||
}
|
||
|
||
// 创建转发请求
|
||
request := sip.NewRequest(sip.MESSAGE, device.Recipient)
|
||
|
||
// 设置From头部,使用平台信息
|
||
fromHeader := device.fromHDR
|
||
fromTag, _ := req.From().Params.Get("tag")
|
||
fromHeader.Params.Add("tag", fromTag)
|
||
request.AppendHeader(&fromHeader)
|
||
|
||
// 添加To头部,使用设备信息
|
||
toHeader := sip.ToHeader{
|
||
Address: device.Recipient,
|
||
}
|
||
request.AppendHeader(&toHeader)
|
||
|
||
// 添加Via头部
|
||
//viaHeader := sip.ViaHeader{
|
||
// ProtocolName: "SIP",
|
||
// ProtocolVersion: "2.0",
|
||
// Transport: device.Transport,
|
||
// Host: device.SipIp,
|
||
// Port: device.LocalPort,
|
||
// Params: sip.NewParams(),
|
||
//}
|
||
//viaHeader.Params.Add("branch", sip.GenerateBranchN(16)).Add("rport", "")
|
||
//request.AppendHeader(&viaHeader)
|
||
|
||
// 设置Content-Type
|
||
contentTypeHeader := sip.ContentTypeHeader("Application/MANSCDP+xml")
|
||
request.AppendHeader(&contentTypeHeader)
|
||
|
||
// 直接使用原始消息体
|
||
request.SetBody(req.Body())
|
||
|
||
// 设置传输协议
|
||
request.SetTransport(strings.ToUpper(device.Transport))
|
||
|
||
// 发送请求
|
||
_, err = device.client.Do(p, request)
|
||
if err != nil {
|
||
p.Error("发送预置位查询命令失败", "error", err.Error())
|
||
return fmt.Errorf("send preset query command failed: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetKey 返回平台的唯一标识符
|
||
func (p *Platform) GetKey() string {
|
||
return p.PlatformModel.ServerGBID
|
||
}
|