diff --git a/pkg/onvif/client.go b/pkg/onvif/client.go index cb6221e1..3b1f065d 100644 --- a/pkg/onvif/client.go +++ b/pkg/onvif/client.go @@ -3,9 +3,10 @@ package onvif import ( "bytes" "errors" + "fmt" "html" "io" - "net/http" + "net" "net/url" "regexp" "strings" @@ -37,13 +38,22 @@ func NewClient(rawURL string) (*Client, error) { client.deviceURL = baseURL + u.Path } + // Set default media URL before trying to get capabilities + client.mediaURL = baseURL + "/onvif/media_service" + client.imaginURL = baseURL + "/onvif/imaging_service" + b, err := client.DeviceRequest(DeviceGetCapabilities) if err != nil { return nil, err } - client.mediaURL = FindTagValue(b, "Media.+?XAddr") - client.imaginURL = FindTagValue(b, "Imaging.+?XAddr") + // Update URLs if found in capabilities + if mediaAddr := FindTagValue(b, "Media.+?XAddr"); mediaAddr != "" { + client.mediaURL = mediaAddr + } + if imagingAddr := FindTagValue(b, "Imaging.+?XAddr"); imagingAddr != "" { + client.imaginURL = imagingAddr + } return client, nil } @@ -172,26 +182,70 @@ func (c *Client) MediaRequest(operation string) ([]byte, error) { return c.Request(c.mediaURL, operation) } -func (c *Client) Request(url, body string) ([]byte, error) { - if url == "" { +func (c *Client) Request(rawUrl, body string) ([]byte, error) { + if rawUrl == "" { return nil, errors.New("onvif: unsupported service") } e := NewEnvelopeWithUser(c.url.User) e.Append(body) - client := &http.Client{Timeout: time.Second * 5000} - res, err := client.Post(url, `application/soap+xml;charset=utf-8`, bytes.NewReader(e.Bytes())) + u, err := url.Parse(rawUrl) if err != nil { return nil, err } - // need to close body with eny response status - b, err := io.ReadAll(res.Body) + // Ensure we have a port + host := u.Host + if !strings.Contains(host, ":") { + host = host + ":80" + } - if err == nil && res.StatusCode != http.StatusOK { - err = errors.New("onvif: " + res.Status + " for " + url) + // Connect with timeout + conn, err := net.DialTimeout("tcp", host, 5*time.Second) + if err != nil { + return nil, err } + defer conn.Close() - return b, err -} + // Send request + httpReq := fmt.Sprintf("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%s", u.Path, u.Host, len(e.Bytes()), e.Bytes()) + + if _, err = conn.Write([]byte(httpReq)); err != nil { + return nil, err + } + + // Read full response first + var fullResponse []byte + buf := make([]byte, 4096) + for { + n, err := conn.Read(buf) + if n > 0 { + fullResponse = append(fullResponse, buf[:n]...) + } + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + } + + // Look for XML in complete response + if idx := bytes.Index(fullResponse, []byte("= 0 { + return fullResponse[idx:], nil + } + + // No XML found - might be an error response + if idx := bytes.Index(fullResponse, []byte("\r\n\r\n")); idx >= 0 { + // Return body after headers + return fullResponse[idx+4:], nil + } + + return fullResponse, nil +} \ No newline at end of file