package onvif import ( "bytes" "crypto/sha1" "encoding/base64" "errors" "github.com/AlexxIT/go2rtc/pkg/core" "html" "io" "net/http" "net/url" "regexp" "strings" "time" ) type Client struct { url *url.URL } func NewClient(rawURL string) (*Client, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err } return &Client{url: u}, nil } func (c *Client) GetURI() (string, error) { query := c.url.Query() token := query.Get("subtype") // support empty if i := atoi(token); i >= 0 { tokens, err := c.GetProfilesTokens() if err != nil { return "", err } if i >= len(tokens) { return "", errors.New("wrong subtype") } token = tokens[i] } getUri := c.GetStreamUri if query.Has("snapshot") { getUri = c.GetSnapshotUri } b, err := getUri(token) if err != nil { return "", err } uri := FindTagValue(b, "Uri") uri = html.UnescapeString(uri) u, err := url.Parse(uri) if err != nil { return "", err } if u.User == nil && c.url.User != nil { u.User = c.url.User } return u.String(), nil } func (c *Client) GetName() (string, error) { b, err := c.GetDeviceInformation() if err != nil { return "", err } return FindTagValue(b, "Manufacturer") + " " + FindTagValue(b, "Model"), nil } func (c *Client) GetProfilesTokens() ([]string, error) { b, err := c.GetProfiles() if err != nil { return nil, err } var tokens []string re := regexp.MustCompile(`Profiles.+?token="([^"]+)`) for _, s := range re.FindAllStringSubmatch(string(b), 10) { tokens = append(tokens, s[1]) } return tokens, nil } func (c *Client) HasSnapshots() bool { b, err := c.GetServiceCapabilities() if err != nil { return false } return strings.Contains(string(b), `SnapshotUri="true"`) } func (c *Client) GetCapabilities() ([]byte, error) { return c.Request( PathDevice, ` All `, ) } func (c *Client) GetNetworkInterfaces() ([]byte, error) { return c.Request( PathDevice, ``, ) } func (c *Client) GetDeviceInformation() ([]byte, error) { return c.Request( PathDevice, ``, ) } func (c *Client) GetProfiles() ([]byte, error) { return c.Request( PathMedia, ``, ) } func (c *Client) GetStreamUri(token string) ([]byte, error) { return c.Request( PathMedia, ` RTP-Unicast RTSP `+token+` `, ) } func (c *Client) GetSnapshotUri(token string) ([]byte, error) { return c.Request( PathMedia, ` `+token+` `, ) } func (c *Client) GetSystemDateAndTime() ([]byte, error) { return c.Request( PathDevice, ``, ) } func (c *Client) GetServiceCapabilities() ([]byte, error) { // some cameras answer GetServiceCapabilities for media only for path = "/onvif/media" return c.Request( PathMedia, ``, ) } func (c *Client) SystemReboot() ([]byte, error) { return c.Request( PathDevice, ``, ) } func (c *Client) GetServices() ([]byte, error) { return c.Request( PathDevice, ` true `, ) } func (c *Client) GetScopes() ([]byte, error) { return c.Request( PathDevice, ``, ) } func (c *Client) Request(path, body string) ([]byte, error) { buf := bytes.NewBuffer(nil) buf.WriteString( ``, ) if user := c.url.User; user != nil { nonce := core.RandString(16, 36) created := time.Now().UTC().Format(time.RFC3339Nano) pass, _ := user.Password() h := sha1.New() h.Write([]byte(nonce + created + pass)) buf.WriteString(` ` + user.Username() + ` ` + base64.StdEncoding.EncodeToString(h.Sum(nil)) + ` ` + base64.StdEncoding.EncodeToString([]byte(nonce)) + ` ` + created + ` `) } buf.WriteString(`` + body + ``) client := &http.Client{Timeout: time.Second * 5000} res, err := client.Post( "http://"+c.url.Host+path, `application/soap+xml;charset=utf-8`, buf, ) if err != nil { return nil, err } // need to close body with eny response status b, err := io.ReadAll(res.Body) if err == nil && res.StatusCode != http.StatusOK { err = errors.New(res.Status) } return b, err }