Add support VIGI cameras #1470

This commit is contained in:
Alex X
2024-12-16 20:24:45 +03:00
parent f1ba5e95ec
commit 8ecaabfce9
3 changed files with 79 additions and 33 deletions
+4
View File
@@ -15,4 +15,8 @@ func Init() {
streams.HandleFunc("tapo", func(source string) (core.Producer, error) { streams.HandleFunc("tapo", func(source string) (core.Producer, error) {
return tapo.Dial(source) return tapo.Dial(source)
}) })
streams.HandleFunc("vigi", func(source string) (core.Producer, error) {
return tapo.Dial(source)
})
} }
+74 -32
View File
@@ -27,7 +27,7 @@ import (
type Client struct { type Client struct {
core.Listener core.Listener
url string url *url.URL
medias []*core.Media medias []*core.Media
receivers []*core.Receiver receivers []*core.Receiver
@@ -52,17 +52,15 @@ type cbcMode interface {
SetIV([]byte) SetIV([]byte)
} }
func Dial(url string) (*Client, error) { // Dial support different urls:
var err error // - tapo://{cloud-password}@192.168.1.123 - auth to Tapo cameras
c := &Client{url: url} // with cloud password (autodetect hash method)
if c.conn1, err = c.newConn(); err != nil { // - tapo://admin:{hashed-cloud-password}@192.168.1.123 - auth to Tapo cameras
return nil, err // with pre-hashed cloud password
} // - vigi://admin:{password}@192.168.1.123 - auth to Vigi cameras with password
return c, nil // for admin account (other not supported)
} func Dial(rawURL string) (*Client, error) {
u, err := url.Parse(rawURL)
func (c *Client) newConn() (net.Conn, error) {
u, err := url.Parse(c.url)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -71,21 +69,31 @@ func (c *Client) newConn() (net.Conn, error) {
u.Host += ":8800" u.Host += ":8800"
} }
req, err := http.NewRequest("POST", "http://"+u.Host+"/stream", nil) c := &Client{url: u}
if c.conn1, err = c.newConn(); err != nil {
return nil, err
}
return c, nil
}
func (c *Client) newConn() (net.Conn, error) {
req, err := http.NewRequest("POST", "http://"+c.url.Host+"/stream", nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
query := u.Query() query := c.url.Query()
if deviceId := query.Get("deviceId"); deviceId != "" { if deviceId := query.Get("deviceId"); deviceId != "" {
req.URL.RawQuery = "deviceId=" + deviceId req.URL.RawQuery = "deviceId=" + deviceId
} }
req.URL.User = u.User
req.Header.Set("Content-Type", "multipart/mixed; boundary=--client-stream-boundary--") req.Header.Set("Content-Type", "multipart/mixed; boundary=--client-stream-boundary--")
conn, res, err := dial(req) username := c.url.User.Username()
password, _ := c.url.User.Password()
conn, res, err := dial(req, c.url.Scheme, username, password)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -95,7 +103,7 @@ func (c *Client) newConn() (net.Conn, error) {
} }
if c.decrypt == nil { if c.decrypt == nil {
c.newDectypter(res) c.newDectypter(res, c.url.Scheme, username, password)
} }
channel := query.Get("channel") channel := query.Get("channel")
@@ -119,14 +127,18 @@ func (c *Client) newConn() (net.Conn, error) {
return conn, nil return conn, nil
} }
func (c *Client) newDectypter(res *http.Response) { func (c *Client) newDectypter(res *http.Response, brand, username, password string) {
username := res.Request.URL.User.Username() exchange := res.Header.Get("Key-Exchange")
password, _ := res.Request.URL.User.Password() nonce := core.Between(exchange, `nonce="`, `"`)
// extract nonce from response if brand == "tapo" && password == "" {
// cipher="AES_128_CBC" username="admin" padding="PKCS7_16" algorithm="MD5" nonce="***" if strings.Contains(exchange, `encrypt_type="3"`) {
nonce := res.Header.Get("Key-Exchange") password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username)))
nonce = core.Between(nonce, `nonce="`, `"`) } else {
password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
}
username = "admin"
}
key := md5.Sum([]byte(nonce + ":" + password)) key := md5.Sum([]byte(nonce + ":" + password))
iv := md5.Sum([]byte(username + ":" + nonce)) iv := md5.Sum([]byte(username + ":" + nonce))
@@ -263,16 +275,12 @@ func (c *Client) Request(conn net.Conn, body []byte) (string, error) {
} }
} }
func dial(req *http.Request) (net.Conn, *http.Response, error) { func dial(req *http.Request, brand, username, password string) (net.Conn, *http.Response, error) {
conn, err := net.DialTimeout("tcp", req.URL.Host, core.ConnDialTimeout) conn, err := net.DialTimeout("tcp", req.URL.Host, core.ConnDialTimeout)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
username := req.URL.User.Username()
password, _ := req.URL.User.Password()
req.URL.User = nil
if err = req.Write(conn); err != nil { if err = req.Write(conn); err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -291,7 +299,7 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) {
return nil, nil, fmt.Errorf("Expected StatusCode to be %d, received %d", http.StatusUnauthorized, res.StatusCode) return nil, nil, fmt.Errorf("Expected StatusCode to be %d, received %d", http.StatusUnauthorized, res.StatusCode)
} }
if password == "" { if brand == "tapo" && password == "" {
// support cloud password in place of username // support cloud password in place of username
if strings.Contains(auth, `encrypt_type="3"`) { if strings.Contains(auth, `encrypt_type="3"`) {
password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username))) password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username)))
@@ -299,6 +307,8 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) {
password = fmt.Sprintf("%16X", md5.Sum([]byte(username))) password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
} }
username = "admin" username = "admin"
} else if brand == "vigi" && username == "admin" {
password = securityEncode(password)
} }
realm := tcp.Between(auth, `realm="`, `"`) realm := tcp.Between(auth, `realm="`, `"`)
@@ -331,7 +341,39 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) {
return nil, nil, err return nil, nil, err
} }
req.URL.User = url.UserPassword(username, password)
return conn, res, nil return conn, res, nil
} }
const (
keyShort = "RDpbLfCPsJZ7fiv"
keyLong = "yLwVl0zKqws7LgKPRQ84Mdt708T1qQ3Ha7xv3H7NyU84p21BriUWBU43odz3iP4rBL3cD02KZciXTysVXiV8ngg6vL48rPJyAUw0HurW20xqxv9aYb4M9wK1Ae0wlro510qXeU07kV57fQMc8L6aLgMLwygtc0F10a0Dg70TOoouyFhdysuRMO51yY5ZlOZZLEal1h0t9YQW0Ko7oBwmCAHoic4HYbUyVeU3sfQ1xtXcPcf1aT303wAQhv66qzW"
)
func securityEncode(s string) string {
size := len(s)
var n int // max
if size > len(keyShort) {
n = size
} else {
n = len(keyShort)
}
b := make([]byte, n)
for i := 0; i < n; i++ {
c1 := 187
c2 := 187
if i >= size {
c1 = int(keyShort[i])
} else if i >= len(keyShort) {
c2 = int(s[i])
} else {
c1 = int(keyShort[i])
c2 = int(s[i])
}
b[i] = keyLong[(c1^c2)%len(keyLong)]
}
return string(b)
}
+1 -1
View File
@@ -77,7 +77,7 @@ func (c *Client) Stop() error {
func (c *Client) MarshalJSON() ([]byte, error) { func (c *Client) MarshalJSON() ([]byte, error) {
info := &core.Connection{ info := &core.Connection{
ID: core.ID(c), ID: core.ID(c),
FormatName: "tapo", FormatName: c.url.Scheme,
Protocol: "http", Protocol: "http",
Medias: c.medias, Medias: c.medias,
Recv: c.recv, Recv: c.recv,