From 6db4dda53539f20211e5e95bbb9d617a19aeef5f Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 23 Oct 2025 14:42:38 +0300 Subject: [PATCH 1/4] Fix onvif client for some cameras --- pkg/onvif/client.go | 89 +++++++++++++++++++++++--------------------- pkg/onvif/helpers.go | 12 ++++++ 2 files changed, 59 insertions(+), 42 deletions(-) diff --git a/pkg/onvif/client.go b/pkg/onvif/client.go index 77bbe0ff..a2358e68 100644 --- a/pkg/onvif/client.go +++ b/pkg/onvif/client.go @@ -1,12 +1,14 @@ package onvif import ( + "bufio" "bytes" "errors" "fmt" "html" "io" "net" + "net/http" "net/url" "regexp" "strings" @@ -32,26 +34,18 @@ func NewClient(rawURL string) (*Client, error) { baseURL := "http://" + u.Host client := &Client{url: u} - if u.Path == "" { - client.deviceURL = baseURL + PathDevice - } else { - client.deviceURL = baseURL + u.Path - } + client.deviceURL = baseURL + GetPath(u.Path, PathDevice) b, err := client.DeviceRequest(DeviceGetCapabilities) if err != nil { return nil, err } - client.mediaURL = FindTagValue(b, "Media.+?XAddr") - if client.mediaURL == "" { - client.mediaURL = baseURL + "/onvif/media_service" - } + s := FindTagValue(b, "Media.+?XAddr") + client.mediaURL = baseURL + GetPath(s, "/onvif/media_service") - client.imaginURL = FindTagValue(b, "Imaging.+?XAddr") - if client.imaginURL == "" { - client.imaginURL = baseURL + "/onvif/imaging_service" - } + s = FindTagValue(b, "Imaging.+?XAddr") + client.imaginURL = baseURL + GetPath(s, "/onvif/imaging_service") return client, nil } @@ -188,9 +182,6 @@ func (c *Client) Request(rawUrl, body string) ([]byte, error) { return nil, errors.New("onvif: unsupported service") } - e := NewEnvelopeWithUser(c.url.User) - e.Append(body) - u, err := url.Parse(rawUrl) if err != nil { return nil, err @@ -201,44 +192,58 @@ func (c *Client) Request(rawUrl, body string) ([]byte, error) { host += ":80" } - conn, err := net.DialTimeout("tcp", host, 5*time.Second) + const timeout = 5 * time.Second + + conn, err := net.DialTimeout("tcp", host, timeout) if err != nil { return nil, err } defer conn.Close() - reqBody := e.Bytes() - rawReq := fmt.Appendf(nil, "POST %s HTTP/1.1\r\n"+ - "Host: %s\r\n"+ - "Content-Type: application/soap+xml;charset=utf-8\r\n"+ - "Content-Length: %d\r\n"+ - "Connection: close\r\n"+ - "\r\n", u.Path, u.Host, len(reqBody)) - rawReq = append(rawReq, reqBody...) + e := NewEnvelopeWithUser(c.url.User) + e.Append(body) + buf := e.Bytes() - if _, err = conn.Write(rawReq); err != nil { + req := &http.Request{ + Method: "POST", + URL: u, + Proto: "HTTP/1.1", + Header: http.Header{"Content-Type": {"application/soap+xml;charset=utf-8"}}, + Body: io.NopCloser(bytes.NewReader(buf)), + ContentLength: int64(len(buf)), + Close: true, + } + + _ = conn.SetWriteDeadline(time.Now().Add(timeout)) + if err = req.Write(conn); err != nil { return nil, err } - rawRes, err := io.ReadAll(conn) + rd := bufio.NewReaderSize(conn, 16*1024) + + _ = conn.SetReadDeadline(time.Now().Add(timeout)) + res, err := http.ReadResponse(rd, req) if err != nil { - return nil, err - } - - // Look for XML in complete response - if i := bytes.Index(rawRes, []byte(" 0 { - return rawRes[i:], nil - } - - // No XML found - might be an error response - if i := bytes.Index(rawRes, []byte("\r\n\r\n")); i > 0 { - if bytes.Contains(rawRes[:i], []byte("chunked")) { - return nil, errors.New("onvif: TODO: support chunked encoding") + // Try to fix broken response https://github.com/AlexxIT/go2rtc/pull/1589 + if buf, err = io.ReadAll(rd); err != nil { + return nil, err } - // Return body after headers - return rawRes[i+4:], nil + // Look for XML in complete response + if i := bytes.Index(buf, []byte(" 0 { + return buf[i:], nil + } + + return nil, fmt.Errorf("onvif: broken response: %.100s", buf) } - return rawRes, nil + if res.StatusCode != http.StatusOK { + return nil, errors.New("onvif: wrong response " + res.Status) + } + + if buf, err = io.ReadAll(res.Body); err != nil { + return nil, err + } + + return buf, nil } diff --git a/pkg/onvif/helpers.go b/pkg/onvif/helpers.go index f240f2ec..893beb00 100644 --- a/pkg/onvif/helpers.go +++ b/pkg/onvif/helpers.go @@ -3,6 +3,7 @@ package onvif import ( "fmt" "net" + "net/url" "regexp" "strconv" "strings" @@ -129,3 +130,14 @@ func GetPosixTZ(current time.Time) string { return prefix + fmt.Sprintf("%02d:%02d", offset/60, offset%60) } + +func GetPath(urlOrPath, defPath string) string { + if urlOrPath == "" || urlOrPath[0] == '/' { + return defPath + } + u, err := url.Parse(urlOrPath) + if err != nil { + return defPath + } + return GetPath(u.Path, defPath) +} From cc97bc33c4399c7979d13702b048db3fd1392c0f Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 24 Oct 2025 17:28:49 +0300 Subject: [PATCH 2/4] Restore simple onvif client logic --- pkg/onvif/client.go | 66 +++++---------------------------------------- 1 file changed, 7 insertions(+), 59 deletions(-) diff --git a/pkg/onvif/client.go b/pkg/onvif/client.go index a2358e68..bad103c7 100644 --- a/pkg/onvif/client.go +++ b/pkg/onvif/client.go @@ -1,13 +1,10 @@ package onvif import ( - "bufio" "bytes" "errors" - "fmt" "html" "io" - "net" "net/http" "net/url" "regexp" @@ -177,73 +174,24 @@ func (c *Client) MediaRequest(operation string) ([]byte, error) { return c.Request(c.mediaURL, operation) } -func (c *Client) Request(rawUrl, body string) ([]byte, error) { - if rawUrl == "" { +func (c *Client) Request(url, body string) ([]byte, error) { + if url == "" { return nil, errors.New("onvif: unsupported service") } - u, err := url.Parse(rawUrl) - if err != nil { - return nil, err - } - - host := u.Host - if u.Port() == "" { - host += ":80" - } - - const timeout = 5 * time.Second - - conn, err := net.DialTimeout("tcp", host, timeout) - if err != nil { - return nil, err - } - defer conn.Close() - e := NewEnvelopeWithUser(c.url.User) e.Append(body) - buf := e.Bytes() - req := &http.Request{ - Method: "POST", - URL: u, - Proto: "HTTP/1.1", - Header: http.Header{"Content-Type": {"application/soap+xml;charset=utf-8"}}, - Body: io.NopCloser(bytes.NewReader(buf)), - ContentLength: int64(len(buf)), - Close: true, - } - - _ = conn.SetWriteDeadline(time.Now().Add(timeout)) - if err = req.Write(conn); err != nil { + client := &http.Client{Timeout: time.Second * 5000} + res, err := client.Post(url, `application/soap+xml;charset=utf-8`, bytes.NewReader(e.Bytes())) + if err != nil { return nil, err } - - rd := bufio.NewReaderSize(conn, 16*1024) - - _ = conn.SetReadDeadline(time.Now().Add(timeout)) - res, err := http.ReadResponse(rd, req) - if err != nil { - // Try to fix broken response https://github.com/AlexxIT/go2rtc/pull/1589 - if buf, err = io.ReadAll(rd); err != nil { - return nil, err - } - - // Look for XML in complete response - if i := bytes.Index(buf, []byte(" 0 { - return buf[i:], nil - } - - return nil, fmt.Errorf("onvif: broken response: %.100s", buf) - } + defer res.Body.Close() if res.StatusCode != http.StatusOK { return nil, errors.New("onvif: wrong response " + res.Status) } - if buf, err = io.ReadAll(res.Body); err != nil { - return nil, err - } - - return buf, nil + return io.ReadAll(res.Body) } From 9f407a754dafecd05aed9709b6503b24d1a15fb7 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 24 Oct 2025 17:54:37 +0300 Subject: [PATCH 3/4] Fix tapo source for some cameras #1918 --- pkg/tapo/client.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/tapo/client.go b/pkg/tapo/client.go index c19267ff..0652140e 100644 --- a/pkg/tapo/client.go +++ b/pkg/tapo/client.go @@ -140,6 +140,12 @@ func (c *Client) newDectypter(res *http.Response, brand, username, password stri username = "admin" } + if strings.Contains(exchange, `username="none"`) { + // https://nvd.nist.gov/vuln/detail/CVE-2022-37255 + username = "none" + password = "TPL075526460603" + } + key := md5.Sum([]byte(nonce + ":" + password)) iv := md5.Sum([]byte(username + ":" + nonce)) From 7254bd4fbc53b2dd7fc1bf9de05be2e975e4b672 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 24 Oct 2025 17:54:55 +0300 Subject: [PATCH 4/4] Code refactoring for tapo source --- pkg/tapo/client.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/tapo/client.go b/pkg/tapo/client.go index 0652140e..be669eb5 100644 --- a/pkg/tapo/client.go +++ b/pkg/tapo/client.go @@ -164,8 +164,9 @@ func (c *Client) newDectypter(res *http.Response, brand, username, password stri cbc.CryptBlocks(b, b) // unpad - padSize := int(b[len(b)-1]) - return b[:len(b)-padSize] + n := len(b) + padSize := int(b[n-1]) + return b[:n-padSize] } } @@ -298,12 +299,12 @@ func dial(req *http.Request, brand, username, password string) (net.Conn, *http. return nil, nil, err } _, _ = io.Copy(io.Discard, res.Body) // discard leftovers - _ = res.Body.Close() // ignore response body + _ = res.Body.Close() // ignore response body auth := res.Header.Get("WWW-Authenticate") if res.StatusCode != http.StatusUnauthorized || !strings.HasPrefix(auth, "Digest") { - return nil, nil, fmt.Errorf("Expected StatusCode to be %d, received %d", http.StatusUnauthorized, res.StatusCode) + return nil, nil, errors.New("tapo: wrond status: " + res.Status) } if brand == "tapo" && password == "" {