diff --git a/pkg/tapo/client.go b/pkg/tapo/client.go index 79f37005..9d1c2c64 100644 --- a/pkg/tapo/client.go +++ b/pkg/tapo/client.go @@ -1,10 +1,12 @@ package tapo import ( + "bufio" "bytes" "crypto/aes" "crypto/cipher" "crypto/md5" + "crypto/sha256" "encoding/json" "errors" "fmt" @@ -14,6 +16,7 @@ import ( "net/http" "net/url" "strconv" + "strings" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mpegts" @@ -62,33 +65,24 @@ func (c *Client) newConn() (net.Conn, error) { return nil, err } - // support raw username/password - username := u.User.Username() - password, _ := u.User.Password() - - // or cloud password in place of username - if password == "" { - password = fmt.Sprintf("%16X", md5.Sum([]byte(username))) - username = "admin" - u.User = url.UserPassword(username, password) + // support raw username/password or cloud password in place of username + if _, ok := u.User.Password(); !ok { + u.User = url.UserPassword("admin", u.User.Username()) } - u.Scheme = "http" - u.Path = "/stream" if u.Port() == "" { u.Host += ":8800" } - // TODO: fix closing connection - ctx, pconn := tcp.WithConn() - req, err := http.NewRequestWithContext(ctx, "POST", u.String(), nil) + req, err := http.NewRequest("POST", "http://"+u.Host+"/stream", nil) if err != nil { return nil, err } + req.URL.User = u.User req.Header.Set("Content-Type", "multipart/mixed; boundary=--client-stream-boundary--") - res, err := tcp.Do(req) + conn, res, err := dial(req) if err != nil { return nil, err } @@ -98,13 +92,16 @@ func (c *Client) newConn() (net.Conn, error) { } if c.decrypt == nil { - c.newDectypter(res, username, password) + c.newDectypter(res) } - return *pconn, nil + return conn, nil } -func (c *Client) newDectypter(res *http.Response, username, password string) { +func (c *Client) newDectypter(res *http.Response) { + username := res.Request.URL.User.Username() + password, _ := res.Request.URL.User.Password() + // extract nonce from response // cipher="AES_128_CBC" username="admin" padding="PKCS7_16" algorithm="MD5" nonce="***" nonce := res.Header.Get("Key-Exchange") @@ -244,3 +241,66 @@ func (c *Client) Request(conn net.Conn, body []byte) (string, error) { return v.Params.SessionID, nil } } + +func dial(req *http.Request) (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 + } + + r := bufio.NewReader(conn) + + res, err := http.ReadResponse(r, req) + if err != nil { + return nil, nil, err + } + + auth := res.Header.Get("WWW-Authenticate") + + if res.StatusCode != http.StatusUnauthorized || !strings.HasPrefix(auth, "Digest") { + return nil, nil, err + } + + if strings.Contains(auth, `encrypt_type="3"`) { + password = fmt.Sprintf("%32X", sha256.Sum256([]byte(password))) + } else { + password = fmt.Sprintf("%16X", md5.Sum([]byte(password))) + } + + realm := tcp.Between(auth, `realm="`, `"`) + nonce := tcp.Between(auth, `nonce="`, `"`) + qop := tcp.Between(auth, `qop="`, `"`) + uri := req.URL.RequestURI() + ha1 := tcp.HexMD5(username, realm, password) + ha2 := tcp.HexMD5(req.Method, uri) + nc := "00000001" + cnonce := "00000001" + response := tcp.HexMD5(ha1, nonce, nc, cnonce, qop, ha2) + + header := fmt.Sprintf( + `Digest username="%s", realm="%s", nonce="%s", uri="%s", qop=%s, nc=%s, cnonce="%s", response="%s"`, + username, realm, nonce, uri, qop, nc, cnonce, response, + ) + + req.Header.Set("Authorization", header) + + if err = req.Write(conn); err != nil { + return nil, nil, err + } + + if res, err = http.ReadResponse(r, req); err != nil { + return nil, nil, err + } + + req.URL.User = url.UserPassword(username, password) + + return conn, res, nil +}