Skip to content

Commit 8ecaabf

Browse files
committed
Add support VIGI cameras #1470
1 parent f1ba5e9 commit 8ecaabf

File tree

3 files changed

+79
-33
lines changed

3 files changed

+79
-33
lines changed

internal/tapo/tapo.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,8 @@ func Init() {
1515
streams.HandleFunc("tapo", func(source string) (core.Producer, error) {
1616
return tapo.Dial(source)
1717
})
18+
19+
streams.HandleFunc("vigi", func(source string) (core.Producer, error) {
20+
return tapo.Dial(source)
21+
})
1822
}

pkg/tapo/client.go

Lines changed: 74 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import (
2727
type Client struct {
2828
core.Listener
2929

30-
url string
30+
url *url.URL
3131

3232
medias []*core.Media
3333
receivers []*core.Receiver
@@ -52,17 +52,15 @@ type cbcMode interface {
5252
SetIV([]byte)
5353
}
5454

55-
func Dial(url string) (*Client, error) {
56-
var err error
57-
c := &Client{url: url}
58-
if c.conn1, err = c.newConn(); err != nil {
59-
return nil, err
60-
}
61-
return c, nil
62-
}
63-
64-
func (c *Client) newConn() (net.Conn, error) {
65-
u, err := url.Parse(c.url)
55+
// Dial support different urls:
56+
// - tapo://{cloud-password}@192.168.1.123 - auth to Tapo cameras
57+
// with cloud password (autodetect hash method)
58+
// - tapo://admin:{hashed-cloud-password}@192.168.1.123 - auth to Tapo cameras
59+
// with pre-hashed cloud password
60+
// - vigi://admin:{password}@192.168.1.123 - auth to Vigi cameras with password
61+
// for admin account (other not supported)
62+
func Dial(rawURL string) (*Client, error) {
63+
u, err := url.Parse(rawURL)
6664
if err != nil {
6765
return nil, err
6866
}
@@ -71,21 +69,31 @@ func (c *Client) newConn() (net.Conn, error) {
7169
u.Host += ":8800"
7270
}
7371

74-
req, err := http.NewRequest("POST", "http://"+u.Host+"/stream", nil)
72+
c := &Client{url: u}
73+
if c.conn1, err = c.newConn(); err != nil {
74+
return nil, err
75+
}
76+
return c, nil
77+
}
78+
79+
func (c *Client) newConn() (net.Conn, error) {
80+
req, err := http.NewRequest("POST", "http://"+c.url.Host+"/stream", nil)
7581
if err != nil {
7682
return nil, err
7783
}
7884

79-
query := u.Query()
85+
query := c.url.Query()
8086

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

85-
req.URL.User = u.User
8691
req.Header.Set("Content-Type", "multipart/mixed; boundary=--client-stream-boundary--")
8792

88-
conn, res, err := dial(req)
93+
username := c.url.User.Username()
94+
password, _ := c.url.User.Password()
95+
96+
conn, res, err := dial(req, c.url.Scheme, username, password)
8997
if err != nil {
9098
return nil, err
9199
}
@@ -95,7 +103,7 @@ func (c *Client) newConn() (net.Conn, error) {
95103
}
96104

97105
if c.decrypt == nil {
98-
c.newDectypter(res)
106+
c.newDectypter(res, c.url.Scheme, username, password)
99107
}
100108

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

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

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

131143
key := md5.Sum([]byte(nonce + ":" + password))
132144
iv := md5.Sum([]byte(username + ":" + nonce))
@@ -263,16 +275,12 @@ func (c *Client) Request(conn net.Conn, body []byte) (string, error) {
263275
}
264276
}
265277

266-
func dial(req *http.Request) (net.Conn, *http.Response, error) {
278+
func dial(req *http.Request, brand, username, password string) (net.Conn, *http.Response, error) {
267279
conn, err := net.DialTimeout("tcp", req.URL.Host, core.ConnDialTimeout)
268280
if err != nil {
269281
return nil, nil, err
270282
}
271283

272-
username := req.URL.User.Username()
273-
password, _ := req.URL.User.Password()
274-
req.URL.User = nil
275-
276284
if err = req.Write(conn); err != nil {
277285
return nil, nil, err
278286
}
@@ -291,14 +299,16 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) {
291299
return nil, nil, fmt.Errorf("Expected StatusCode to be %d, received %d", http.StatusUnauthorized, res.StatusCode)
292300
}
293301

294-
if password == "" {
302+
if brand == "tapo" && password == "" {
295303
// support cloud password in place of username
296304
if strings.Contains(auth, `encrypt_type="3"`) {
297305
password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username)))
298306
} else {
299307
password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
300308
}
301309
username = "admin"
310+
} else if brand == "vigi" && username == "admin" {
311+
password = securityEncode(password)
302312
}
303313

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

334-
req.URL.User = url.UserPassword(username, password)
335-
336344
return conn, res, nil
337345
}
346+
347+
const (
348+
keyShort = "RDpbLfCPsJZ7fiv"
349+
keyLong = "yLwVl0zKqws7LgKPRQ84Mdt708T1qQ3Ha7xv3H7NyU84p21BriUWBU43odz3iP4rBL3cD02KZciXTysVXiV8ngg6vL48rPJyAUw0HurW20xqxv9aYb4M9wK1Ae0wlro510qXeU07kV57fQMc8L6aLgMLwygtc0F10a0Dg70TOoouyFhdysuRMO51yY5ZlOZZLEal1h0t9YQW0Ko7oBwmCAHoic4HYbUyVeU3sfQ1xtXcPcf1aT303wAQhv66qzW"
350+
)
351+
352+
func securityEncode(s string) string {
353+
size := len(s)
354+
355+
var n int // max
356+
if size > len(keyShort) {
357+
n = size
358+
} else {
359+
n = len(keyShort)
360+
}
361+
362+
b := make([]byte, n)
363+
364+
for i := 0; i < n; i++ {
365+
c1 := 187
366+
c2 := 187
367+
if i >= size {
368+
c1 = int(keyShort[i])
369+
} else if i >= len(keyShort) {
370+
c2 = int(s[i])
371+
} else {
372+
c1 = int(keyShort[i])
373+
c2 = int(s[i])
374+
}
375+
b[i] = keyLong[(c1^c2)%len(keyLong)]
376+
}
377+
378+
return string(b)
379+
}

pkg/tapo/producer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func (c *Client) Stop() error {
7777
func (c *Client) MarshalJSON() ([]byte, error) {
7878
info := &core.Connection{
7979
ID: core.ID(c),
80-
FormatName: "tapo",
80+
FormatName: c.url.Scheme,
8181
Protocol: "http",
8282
Medias: c.medias,
8383
Recv: c.recv,

0 commit comments

Comments
 (0)