Add support VIGI cameras #1470
This commit is contained in:
@@ -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
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user