This commit is contained in:
xugo
2026-01-04 10:42:05 +08:00
parent 7a2d432256
commit 16b1e8477a
21 changed files with 249 additions and 117 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ func Run(bc *conf.Bootstrap) {
go setupZLM(ctx, bc.ConfigDir)
// 如果需要执行表迁移,递增此版本号和表更新说明
versionapi.DBVersion = "0.0.17"
versionapi.DBVersion = "0.0.18"
versionapi.DBRemark = "onvif device support"
handler, cleanUp, err := wireApp(bc, log)
+1 -1
View File
@@ -9,7 +9,7 @@ type Ext struct {
}
// Scan implements orm.Scaner.
func (i *Ext) Scan(input interface{}) error {
func (i *Ext) Scan(input any) error {
return orm.JSONUnmarshal(input, i)
}
+9 -1
View File
@@ -38,13 +38,21 @@ type DeviceExt struct {
Firmware string `json:"firmware"` // 固件版本
Name string `json:"name"` // 设备名
GBVersion string `json:"gb_version"` // GB版本
Zones []Zone `json:"zones"` // 区域
}
// Scan implements orm.Scaner.
func (i *DeviceExt) Scan(input interface{}) error {
func (i *DeviceExt) Scan(input any) error {
return orm.JSONUnmarshal(input, i)
}
func (i DeviceExt) Value() (driver.Value, error) {
return json.Marshal(i)
}
type Zone struct {
Name string `json:"name"` // 区域名称
Coordinates []float32 `json:"coordinates"` // 坐标
Color string `json:"color"` // 颜色,支持 hex 颜色值,如 #FF0000
Labels []string `json:"labels"` // 标签
}
+2 -7
View File
@@ -6,15 +6,10 @@ import (
// Device 设备接口
// 注意: 适配器实现时,参数类型为 *ipc.Device,满足此接口
type Device interface {
// 这里不定义任何方法,让所有类型都能满足
// 适配器实现时直接使用 *ipc.Device 类型
}
type Device any
// Channel 通道接口
type Channel interface {
// 同上
}
type Channel any
// Protocol 协议抽象接口(端口)
//
+29 -29
View File
@@ -83,32 +83,32 @@ func (d *ZLMDriver) Setup(ctx context.Context, ms *MediaServer, webhookURL strin
// 构造配置请求
req := zlm.SetServerConfigRequest{
RtcExternIP: zlm.NewString(ms.IP),
GeneralMediaServerID: zlm.NewString(ms.ID),
HookEnable: zlm.NewString("1"),
HookOnFlowReport: zlm.NewString(""),
HookOnPlay: zlm.NewString(fmt.Sprintf("%s/on_play", webhookURL)),
RtcExternIP: new(ms.IP),
GeneralMediaServerID: new(ms.ID),
HookEnable: new("1"),
HookOnFlowReport: new(""),
HookOnPlay: new(fmt.Sprintf("%s/on_play", webhookURL)),
ProtocolEnableTs: zlm.NewString("0"),
ProtocolEnableFmp4: zlm.NewString("0"),
ProtocolEnableHls: zlm.NewString("0"),
ProtocolEnableHlsFmp4: zlm.NewString("1"),
ProtocolEnableTs: new("0"),
ProtocolEnableFmp4: new("0"),
ProtocolEnableHls: new("0"),
ProtocolEnableHlsFmp4: new("1"),
HookOnPublish: zlm.NewString(fmt.Sprintf("%s/on_publish", webhookURL)),
HookOnStreamNoneReader: zlm.NewString(fmt.Sprintf("%s/on_stream_none_reader", webhookURL)),
GeneralStreamNoneReaderDelayMS: zlm.NewString("30000"),
HookOnStreamNotFound: zlm.NewString(fmt.Sprintf("%s/on_stream_not_found", webhookURL)),
HookOnRecordTs: zlm.NewString(""),
HookOnRtspAuth: zlm.NewString(""),
HookOnRtspRealm: zlm.NewString(""),
HookOnShellLogin: zlm.NewString(""),
HookOnStreamChanged: zlm.NewString(fmt.Sprintf("%s/on_stream_changed", webhookURL)),
HookOnServerKeepalive: zlm.NewString(fmt.Sprintf("%s/on_server_keepalive", webhookURL)),
HookTimeoutSec: zlm.NewString("10"),
HookAliveInterval: zlm.NewString(fmt.Sprint(ms.HookAliveInterval)),
ProtocolContinuePushMs: zlm.NewString("3000"),
HookOnPublish: new(fmt.Sprintf("%s/on_publish", webhookURL)),
HookOnStreamNoneReader: new(fmt.Sprintf("%s/on_stream_none_reader", webhookURL)),
GeneralStreamNoneReaderDelayMS: new("30000"),
HookOnStreamNotFound: new(fmt.Sprintf("%s/on_stream_not_found", webhookURL)),
HookOnRecordTs: new(""),
HookOnRtspAuth: new(""),
HookOnRtspRealm: new(""),
HookOnShellLogin: new(""),
HookOnStreamChanged: new(fmt.Sprintf("%s/on_stream_changed", webhookURL)),
HookOnServerKeepalive: new(fmt.Sprintf("%s/on_server_keepalive", webhookURL)),
HookTimeoutSec: new("10"),
HookAliveInterval: new(fmt.Sprint(ms.HookAliveInterval)),
ProtocolContinuePushMs: new("3000"),
RtpProxyPortRange: &ms.RTPPortRange,
FfmpegLog: zlm.NewString("./fflogs/ffmpeg.log"),
FfmpegLog: new("./fflogs/ffmpeg.log"),
}
resp, err := engine.SetServerConfig(&req)
@@ -147,12 +147,12 @@ func (d *ZLMDriver) AddStreamProxy(ctx context.Context, ms *MediaServer, req *Ad
RTPType: req.RTPType,
RetryCount: 3,
TimeoutSec: PullTimeoutMs / 1000,
EnableHLSFMP4: zlm.NewBool(true),
EnableAudio: zlm.NewBool(true),
EnableRTSP: zlm.NewBool(true),
EnableRTMP: zlm.NewBool(true),
AddMuteAudio: zlm.NewBool(true),
AutoClose: zlm.NewBool(true),
EnableHLSFMP4: new(true),
EnableAudio: new(true),
EnableRTSP: new(true),
EnableRTMP: new(true),
AddMuteAudio: new(true),
AutoClose: new(true),
})
}
+1 -1
View File
@@ -24,7 +24,7 @@ type MediaServerPorts struct {
}
// Scan implements orm.Scaner.
func (i *MediaServerPorts) Scan(input interface{}) error {
func (i *MediaServerPorts) Scan(input any) error {
return orm.JSONUnmarshal(input, i)
}
+1 -1
View File
@@ -1,3 +1,3 @@
package sms
type NodeServicer interface{}
type NodeServicer any
+17
View File
@@ -103,6 +103,8 @@ func registerGB28181(g gin.IRouter, api IPCAPI, handler ...gin.HandlerFunc) {
group.POST("/:id/play", web.WrapH(api.play)) // 播放(所有协议)
group.POST("/:id/snapshot", web.WrapH(api.refreshSnapshot)) // 图像抓拍(所有协议)
group.GET("/:id/snapshot", api.getSnapshot) // 获取图像(所有协议)
group.POST("/:id/zones", web.WrapH(api.addZone)) // 添加区域(所有协议)
group.GET("/:id/zones", web.WrapH(api.getZones)) // 获取区域(所有协议)
}
}
@@ -285,6 +287,7 @@ func (a IPCAPI) play(c *gin.Context, _ *struct{}) (*playOutput, error) {
for range 2 {
time.Sleep(3 * time.Second)
rtsp := fmt.Sprintf("rtsp://%s:%d/%s", "127.0.0.1", svr.Ports.RTSP, stream) + "?" + session
body, err := a.uc.SMSAPI.smsCore.GetSnapshot(svr, sms.GetSnapRequest{
GetSnapRequest: zlm.GetSnapRequest{
URL: rtsp,
@@ -362,6 +365,20 @@ func (a IPCAPI) refreshSnapshot(c *gin.Context, in *refreshSnapshotInput) (any,
return gin.H{"link": fmt.Sprintf("%s/channels/%s/snapshot?token=%s", prefix, channelID, token)}, nil
}
func (a IPCAPI) addZone(c *gin.Context, in *ipc.AddZoneInput) (gin.H, error) {
channelID := c.Param("id")
if len(in.Labels) == 0 {
in.Labels = []string{"person", "car", "cat", "dog"}
}
zones, err := a.ipc.AddZone(c.Request.Context(), in, channelID)
return gin.H{"items": zones}, err
}
func (a IPCAPI) getZones(c *gin.Context, _ *struct{}) (any, error) {
channelID := c.Param("id")
return a.ipc.GetZones(c.Request.Context(), channelID)
}
func (a IPCAPI) getSnapshot(c *gin.Context) {
channelID := c.Param("id")
body, err := readCover(a.uc.Conf.ConfigDir, channelID)
+100 -10
View File
@@ -1,6 +1,15 @@
package api
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"sync"
"time"
"github.com/gin-gonic/gin"
@@ -10,25 +19,85 @@ import (
)
type UserAPI struct {
conf *conf.Bootstrap
conf *conf.Bootstrap
secret *Secret
}
type Secret struct {
privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey
expiredAt time.Time
m sync.RWMutex
}
// TODO: 有概率存在过期导致登录解密识别
func (s *Secret) GetOrCreatePublicKey() (*rsa.PublicKey, error) {
s.m.RLock()
if s.publicKey != nil && time.Now().Before(s.expiredAt) {
s.m.RUnlock()
return s.publicKey, nil
}
s.m.RUnlock()
s.m.Lock()
defer s.m.Unlock()
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
s.privateKey = privateKey
s.publicKey = &privateKey.PublicKey
s.expiredAt = time.Now().Add(1 * time.Hour)
return s.publicKey, nil
}
func (s *Secret) MarshalPKIXPublicKey(key *rsa.PublicKey) []byte {
publicKeyBytes, _ := x509.MarshalPKIXPublicKey(key)
return pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: publicKeyBytes,
})
}
func (s *Secret) Decrypt(ciphertext string) ([]byte, error) {
s.m.RLock()
pri := s.privateKey
s.m.RUnlock()
if pri == nil {
return nil, fmt.Errorf("请刷新页面后重试")
}
data, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return nil, err
}
plaintext, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, pri, data, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
func NewUserAPI(conf *conf.Bootstrap) UserAPI {
return UserAPI{
conf: conf,
conf: conf,
secret: &Secret{},
}
}
func RegisterUser(r gin.IRouter, api UserAPI, mid ...gin.HandlerFunc) {
group := r.Group("/user")
group.POST("/login", web.WrapH(api.login))
group.PUT("/user", web.WrapHs(api.updateCredentials, mid...)...)
r.POST("/login", web.WrapH(api.login))
r.GET("/login/key", web.WrapH(api.getPublicKey))
group := r.Group("/users", mid...)
group.PUT("", web.WrapHs(api.updateCredentials, mid...)...)
}
// 登录请求结构体
type loginInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
// Username string `json:"username" binding:"required"`
// Password string `json:"password" binding:"required"`
Data string `json:"data" binding:"required"`
}
// 登录响应结构体
@@ -39,16 +108,28 @@ type loginOutput struct {
// 登录接口
func (api UserAPI) login(_ *gin.Context, in *loginInput) (*loginOutput, error) {
body, err := api.secret.Decrypt(in.Data)
if err != nil {
return nil, reason.ErrServer.SetMsg(err.Error())
}
var credentials struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.Unmarshal(body, &credentials); err != nil {
return nil, reason.ErrServer.SetMsg(err.Error())
}
// 验证用户名和密码
if api.conf.Server.Username == "" && api.conf.Server.Password == "" {
api.conf.Server.Username = "admin"
api.conf.Server.Password = "admin"
}
if in.Username != api.conf.Server.Username || in.Password != api.conf.Server.Password {
if credentials.Username != api.conf.Server.Username || credentials.Password != api.conf.Server.Password {
return nil, reason.ErrNameOrPasswd
}
data := web.NewClaimsData().SetUsername(in.Username)
data := web.NewClaimsData().SetUsername(credentials.Username)
token, err := web.NewToken(data, api.conf.Server.HTTP.JwtSecret, web.WithExpiresAt(time.Now().Add(3*24*time.Hour)))
if err != nil {
@@ -57,7 +138,7 @@ func (api UserAPI) login(_ *gin.Context, in *loginInput) (*loginOutput, error) {
return &loginOutput{
Token: token,
User: in.Username,
User: credentials.Username,
}, nil
}
@@ -80,3 +161,12 @@ func (api UserAPI) updateCredentials(_ *gin.Context, in *updateCredentialsInput)
return gin.H{"msg": "凭据更新成功"}, nil
}
func (api UserAPI) getPublicKey(_ *gin.Context, _ *struct{}) (gin.H, error) {
publicKey, err := api.secret.GetOrCreatePublicKey()
if err != nil {
return nil, reason.ErrServer.SetMsg(err.Error())
}
result := api.secret.MarshalPKIXPublicKey(publicKey)
return gin.H{"key": base64.StdEncoding.EncodeToString(result)}, nil
}