Skip to content

Commit

Permalink
Add support VIGI cameras #1470
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexxIT committed Dec 16, 2024
1 parent f1ba5e9 commit 8ecaabf
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 33 deletions.
4 changes: 4 additions & 0 deletions internal/tapo/tapo.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ func Init() {
streams.HandleFunc("tapo", func(source string) (core.Producer, error) {
return tapo.Dial(source)
})

streams.HandleFunc("vigi", func(source string) (core.Producer, error) {
return tapo.Dial(source)
})
}
106 changes: 74 additions & 32 deletions pkg/tapo/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
type Client struct {
core.Listener

url string
url *url.URL

medias []*core.Media
receivers []*core.Receiver
Expand All @@ -52,17 +52,15 @@ type cbcMode interface {
SetIV([]byte)
}

func Dial(url string) (*Client, error) {
var err error
c := &Client{url: url}
if c.conn1, err = c.newConn(); err != nil {
return nil, err
}
return c, nil
}

func (c *Client) newConn() (net.Conn, error) {
u, err := url.Parse(c.url)
// Dial support different urls:
// - tapo://{cloud-password}@192.168.1.123 - auth to Tapo cameras
// with cloud password (autodetect hash method)
// - tapo://admin:{hashed-cloud-password}@192.168.1.123 - auth to Tapo cameras
// with pre-hashed cloud password
// - vigi://admin:{password}@192.168.1.123 - auth to Vigi cameras with password
// for admin account (other not supported)
func Dial(rawURL string) (*Client, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
Expand All @@ -71,21 +69,31 @@ func (c *Client) newConn() (net.Conn, error) {
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 {
return nil, err
}

query := u.Query()
query := c.url.Query()

if deviceId := query.Get("deviceId"); deviceId != "" {
req.URL.RawQuery = "deviceId=" + deviceId
}

req.URL.User = u.User
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 {
return nil, err
}
Expand All @@ -95,7 +103,7 @@ func (c *Client) newConn() (net.Conn, error) {
}

if c.decrypt == nil {
c.newDectypter(res)
c.newDectypter(res, c.url.Scheme, username, password)
}

channel := query.Get("channel")
Expand All @@ -119,14 +127,18 @@ func (c *Client) newConn() (net.Conn, error) {
return conn, nil
}

func (c *Client) newDectypter(res *http.Response) {
username := res.Request.URL.User.Username()
password, _ := res.Request.URL.User.Password()
func (c *Client) newDectypter(res *http.Response, brand, username, password string) {
exchange := res.Header.Get("Key-Exchange")
nonce := core.Between(exchange, `nonce="`, `"`)

// extract nonce from response
// cipher="AES_128_CBC" username="admin" padding="PKCS7_16" algorithm="MD5" nonce="***"
nonce := res.Header.Get("Key-Exchange")
nonce = core.Between(nonce, `nonce="`, `"`)
if brand == "tapo" && password == "" {
if strings.Contains(exchange, `encrypt_type="3"`) {
password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username)))
} else {
password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
}
username = "admin"
}

key := md5.Sum([]byte(nonce + ":" + password))
iv := md5.Sum([]byte(username + ":" + nonce))
Expand Down Expand Up @@ -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)
if err != nil {
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 {
return nil, nil, err
}
Expand All @@ -291,14 +299,16 @@ 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)
}

if password == "" {
if brand == "tapo" && password == "" {
// support cloud password in place of username
if strings.Contains(auth, `encrypt_type="3"`) {
password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username)))
} else {
password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
}
username = "admin"
} else if brand == "vigi" && username == "admin" {
password = securityEncode(password)
}

realm := tcp.Between(auth, `realm="`, `"`)
Expand Down Expand Up @@ -331,7 +341,39 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) {
return nil, nil, err
}

req.URL.User = url.UserPassword(username, password)

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)
}
2 changes: 1 addition & 1 deletion pkg/tapo/producer.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func (c *Client) Stop() error {
func (c *Client) MarshalJSON() ([]byte, error) {
info := &core.Connection{
ID: core.ID(c),
FormatName: "tapo",
FormatName: c.url.Scheme,
Protocol: "http",
Medias: c.medias,
Recv: c.recv,
Expand Down

0 comments on commit 8ecaabf

Please sign in to comment.