110 lines
3.4 KiB
Go
110 lines
3.4 KiB
Go
package onvif
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Credential struct {
|
|
Username string
|
|
Password string
|
|
}
|
|
type Profile struct {
|
|
Token string `json:"token"`
|
|
Name string `json:"name"`
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
Encoding string `json:"encoding"`
|
|
StreamURI string `json:"stream_uri"`
|
|
}
|
|
type Client interface {
|
|
Profiles(context.Context, string, Credential) ([]Profile, error)
|
|
}
|
|
|
|
type HTTPClient struct{ client *http.Client }
|
|
|
|
func NewHTTPClient(timeout time.Duration) *HTTPClient {
|
|
if timeout <= 0 {
|
|
timeout = 8 * time.Second
|
|
}
|
|
return &HTTPClient{client: &http.Client{Timeout: timeout}}
|
|
}
|
|
func (c *HTTPClient) Profiles(ctx context.Context, address string, credential Credential) ([]Profile, error) {
|
|
endpoint, err := validateEndpoint(address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
body := `<?xml version="1.0"?><s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"><s:Body><GetProfiles xmlns="http://www.onvif.org/ver10/media/wsdl"/></s:Body></s:Envelope>`
|
|
data, err := c.soap(ctx, endpoint, credential, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
profiles, err := ParseProfiles(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range profiles {
|
|
uriBody := fmt.Sprintf(`<?xml version="1.0"?><s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"><s:Body><GetStreamUri xmlns="http://www.onvif.org/ver10/media/wsdl"><StreamSetup><Stream xmlns="http://www.onvif.org/ver10/schema">RTP-Unicast</Stream><Transport xmlns="http://www.onvif.org/ver10/schema"><Protocol>RTSP</Protocol></Transport></StreamSetup><ProfileToken>%s</ProfileToken></GetStreamUri></s:Body></s:Envelope>`, xmlEscape(profiles[i].Token))
|
|
streamData, err := c.soap(ctx, endpoint, credential, uriBody)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
profiles[i].StreamURI, err = ParseStreamURI(streamData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return profiles, nil
|
|
}
|
|
func (c *HTTPClient) soap(ctx context.Context, endpoint string, credential Credential, body string) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBufferString(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/soap+xml; charset=utf-8")
|
|
if credential.Username != "" {
|
|
req.SetBasicAuth(credential.Username, credential.Password)
|
|
}
|
|
res, err := c.client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("onvif request failed: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
data, err := io.ReadAll(io.LimitReader(res.Body, 2<<20))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if res.StatusCode == http.StatusUnauthorized {
|
|
return nil, fmt.Errorf("authentication_failed")
|
|
}
|
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
|
return nil, fmt.Errorf("onvif_http_%d", res.StatusCode)
|
|
}
|
|
return data, nil
|
|
}
|
|
func validateEndpoint(value string) (string, error) {
|
|
parsed, err := url.Parse(value)
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return "", fmt.Errorf("invalid ONVIF address")
|
|
}
|
|
if parsed.User != nil {
|
|
return "", fmt.Errorf("credentials must not appear in ONVIF address")
|
|
}
|
|
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
|
return "", fmt.Errorf("unsupported ONVIF scheme")
|
|
}
|
|
return parsed.String(), nil
|
|
}
|
|
func xmlEscape(value string) string {
|
|
var b strings.Builder
|
|
_ = xml.EscapeText(&b, []byte(value))
|
|
return b.String()
|
|
}
|