Files
onvif/Device.go
Cedric Verstraeten ede8b81fc8 Update Device.go
2025-01-19 09:58:53 +01:00

471 lines
14 KiB
Go

package onvif
import (
"bytes"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
"github.com/kerberos-io/onvif/networking"
"github.com/kerberos-io/onvif/xsd/onvif"
"github.com/beevik/etree"
"github.com/kerberos-io/onvif/device"
"github.com/kerberos-io/onvif/gosoap"
)
// Xlmns XML Scheam
var Xlmns = map[string]string{
"onvif": "http://www.onvif.org/ver10/schema",
"tds": "http://www.onvif.org/ver10/device/wsdl",
"trt": "http://www.onvif.org/ver10/media/wsdl",
"tr2": "http://www.onvif.org/ver20/media/wsdl",
"tev": "http://www.onvif.org/ver10/events/wsdl",
"tptz": "http://www.onvif.org/ver20/ptz/wsdl",
"timg": "http://www.onvif.org/ver20/imaging/wsdl",
"tan": "http://www.onvif.org/ver20/analytics/wsdl",
"xmime": "http://www.w3.org/2005/05/xmlmime",
"wsnt": "http://docs.oasis-open.org/wsn/b-2",
"xop": "http://www.w3.org/2004/08/xop/include",
"wsa": "http://www.w3.org/2005/08/addressing",
"wstop": "http://docs.oasis-open.org/wsn/t-1",
"wsntw": "http://docs.oasis-open.org/wsn/bw-2",
"wsrf-rw": "http://docs.oasis-open.org/wsrf/rw-2",
"wsaw": "http://www.w3.org/2006/05/addressing/wsdl",
"tt": "http://www.onvif.org/ver10/recording/wsdl",
"wsse": "http://docs.oasis-open.org/wss/2004/01/oasis-200401",
"wsu": "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
}
// DeviceType alias for int
type DeviceType int
// Onvif Device Tyoe
const (
NVD DeviceType = iota
NVS
NVA
NVT
ContentType = "Content-Type"
)
func (devType DeviceType) String() string {
stringRepresentation := []string{
"NetworkVideoDisplay",
"NetworkVideoStorage",
"NetworkVideoAnalytics",
"NetworkVideoTransmitter",
}
i := uint8(devType)
switch {
case i <= uint8(NVT):
return stringRepresentation[i]
default:
return strconv.Itoa(int(i))
}
}
// DeviceInfo struct contains general information about ONVIF device
type DeviceInfo struct {
Name string
Manufacturer string
Model string
FirmwareVersion string
SerialNumber string
HardwareId string
}
// Device for a new device of onvif and DeviceInfo
// struct represents an abstract ONVIF device.
// It contains methods, which helps to communicate with ONVIF device
type Device struct {
params DeviceParams
endpoints map[string]string
info DeviceInfo
digestClient *DigestClient
}
type DeviceParams struct {
Xaddr string
EndpointRefAddress string
Username string
Password string
HttpClient *http.Client
AuthMode string
}
// GetServices return available endpoints
func (dev *Device) GetServices() map[string]string {
return dev.endpoints
}
// GetServices return available endpoints
func (dev *Device) GetDeviceInfo() DeviceInfo {
return dev.info
}
// SetDeviceInfoFromScopes goes through the scopes and sets the device info fields for supported categories (currently name and hardware).
// See 7.3.2.2 Scopes in the ONVIF Core Specification (https://www.onvif.org/specs/core/ONVIF-Core-Specification.pdf).
func (dev *Device) SetDeviceInfoFromScopes(scopes []string) {
newInfo := dev.info
supportedScopes := []struct {
category string
setField func(s string)
}{
{category: "name", setField: func(s string) { newInfo.Name = s }},
{category: "hardware", setField: func(s string) { newInfo.Model = s }},
}
for _, s := range scopes {
for _, supp := range supportedScopes {
fullScope := fmt.Sprintf("onvif://www.onvif.org/%s/", supp.category)
scopeValue, matchesScope := strings.CutPrefix(s, fullScope)
if matchesScope {
unescaped, err := url.QueryUnescape(scopeValue)
if err != nil {
continue
}
supp.setField(unescaped)
}
}
}
dev.info = newInfo
}
func readResponse(resp *http.Response) string {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
return string(b)
}
func (dev *Device) getSupportedServices(resp *http.Response) {
doc := etree.NewDocument()
data, _ := ioutil.ReadAll(resp.Body)
if err := doc.ReadFromBytes(data); err != nil {
//log.Println(err.Error())
return
}
services := doc.FindElements("./Envelope/Body/GetCapabilitiesResponse/Capabilities/*/XAddr")
for _, j := range services {
dev.addEndpoint(j.Parent().Tag, j.Text())
}
extensionServices := doc.FindElements("./Envelope/Body/GetCapabilitiesResponse/Capabilities/Extension/*/XAddr")
for _, j := range extensionServices {
dev.addEndpoint(j.Parent().Tag, j.Text())
}
}
// NewDevice function construct a ONVIF Device entity
func NewDevice(params DeviceParams) (*Device, error) {
dev := new(Device)
dev.params = params
dev.endpoints = make(map[string]string)
dev.addEndpoint("Device", "http://"+dev.params.Xaddr+"/onvif/device_service")
if dev.params.HttpClient == nil {
dev.params.HttpClient = new(http.Client)
}
dev.digestClient = NewDigestClient(dev.params.HttpClient, dev.params.Username, dev.params.Password)
getCapabilities := device.GetCapabilities{Category: []onvif.CapabilityCategory{"All"}}
resp, err := dev.CallMethod(getCapabilities)
if err != nil || resp.StatusCode != http.StatusOK {
return nil, errors.New("camera is not available at " + dev.params.Xaddr + " or it does not support ONVIF services")
}
dev.getSupportedServices(resp)
return dev, nil
}
func (dev *Device) addEndpoint(Key, Value string) {
//use lowCaseKey
//make key having ability to handle Mixed Case for Different vendor devcie (e.g. Events EVENTS, events)
lowCaseKey := strings.ToLower(Key)
// Replace host with host from device params.
if u, err := url.Parse(Value); err == nil {
u.Host = dev.params.Xaddr
Value = u.String()
}
dev.endpoints[lowCaseKey] = Value
if lowCaseKey == strings.ToLower(MediaWebService) {
// Media2 uses the same endpoint but different XML name space
dev.endpoints[strings.ToLower(Media2WebService)] = Value
}
}
// GetEndpoint returns specific ONVIF service endpoint address
func (dev *Device) GetEndpoint(name string) string {
return dev.endpoints[name]
}
func (dev *Device) buildMethodSOAP(msg string) (gosoap.SoapMessage, error) {
doc := etree.NewDocument()
if err := doc.ReadFromString(msg); err != nil {
//log.Println("Got error")
return "", err
}
element := doc.Root()
soap := gosoap.NewEmptySOAP()
soap.AddBodyContent(element)
return soap, nil
}
// getEndpoint functions get the target service endpoint in a better way
func (dev *Device) getEndpoint(endpoint string) (string, error) {
// common condition, endpointMark in map we use this.
if endpointURL, bFound := dev.endpoints[endpoint]; bFound {
return endpointURL, nil
}
//but ,if we have endpoint like event、analytic
//and sametime the Targetkey like : events、analytics
//we use fuzzy way to find the best match url
var endpointURL string
for targetKey := range dev.endpoints {
if strings.Contains(targetKey, endpoint) {
endpointURL = dev.endpoints[targetKey]
return endpointURL, nil
}
}
return endpointURL, errors.New("target endpoint service not found")
}
// CallMethod functions call an method, defined <method> struct.
// You should use Authenticate method to call authorized requests.
func (dev Device) CallMethod(method interface{}) (*http.Response, error) {
pkgPath := strings.Split(reflect.TypeOf(method).PkgPath(), "/")
pkg := strings.ToLower(pkgPath[len(pkgPath)-1])
endpoint, err := dev.getEndpoint(pkg)
if err != nil {
return nil, err
}
return dev.callMethodDo(endpoint, method)
}
// CallMethod functions call an method, defined <method> struct with authentication data
func (dev Device) callMethodDo(endpoint string, method interface{}) (*http.Response, error) {
output, err := xml.MarshalIndent(method, " ", " ")
if err != nil {
return nil, err
}
soap, err := dev.buildMethodSOAP(string(output))
if err != nil {
return nil, err
}
soap.AddRootNamespaces(Xlmns)
soap.AddAction()
//Auth Handling
if dev.params.Username != "" && dev.params.Password != "" {
soap.AddWSSecurity(dev.params.Username, dev.params.Password)
}
servResp, err := networking.SendSoap(dev.params.HttpClient, endpoint, soap.String())
if err != nil {
// Close server response body to reuse the connection
if servResp != nil {
servResp.Body.Close()
}
servResp, err = networking.SendSoapWithDigest(dev.params.HttpClient, endpoint, soap.String(), dev.params.Username, dev.params.Password)
}
return servResp, err
}
func (dev *Device) GetDeviceParams() DeviceParams {
return dev.params
}
func (dev *Device) GetEndpointByRequestStruct(requestStruct interface{}) (string, error) {
pkgPath := strings.Split(reflect.TypeOf(requestStruct).Elem().PkgPath(), "/")
pkg := strings.ToLower(pkgPath[len(pkgPath)-1])
endpoint, err := dev.getEndpoint(pkg)
if err != nil {
return "", err
}
return endpoint, err
}
/*func (dev *Device) SendSoap(endpoint string, xmlRequestBody string) (resp *http.Response, err error) {
soap := gosoap.NewEmptySOAP()
soap.AddStringBodyContent(xmlRequestBody)
soap.AddRootNamespaces(Xlmns)
if dev.params.AuthMode == UsernameTokenAuth || dev.params.AuthMode == Both {
soap.AddWSSecurity(dev.params.Username, dev.params.Password)
}
if dev.params.AuthMode == DigestAuth || dev.params.AuthMode == Both {
resp, err = dev.digestClient.Do(http.MethodPost, endpoint, soap.String())
} else {
var req *http.Request
req, err = createHttpRequest(http.MethodPost, endpoint, soap.String())
if err != nil {
return nil, err
}
resp, err = dev.params.HttpClient.Do(req)
}
return resp, err
}*/
// CallMethod functions call an method, defined <method> struct with authentication data
func (dev Device) SendSoap(endpoint string, xmlRequestBody string) (*http.Response, error) {
soap := gosoap.NewEmptySOAP()
soap.AddStringBodyContent(xmlRequestBody)
soap.AddRootNamespaces(Xlmns)
soap.AddAction()
//Auth Handling
if dev.params.Username != "" && dev.params.Password != "" {
soap.AddWSSecurity(dev.params.Username, dev.params.Password)
}
servResp, err := networking.SendSoap(dev.params.HttpClient, endpoint, soap.String())
if err != nil {
// Close server response body to reuse the connection
if servResp != nil {
servResp.Body.Close()
}
servResp, err = networking.SendSoapWithDigest(dev.params.HttpClient, endpoint, soap.String(), dev.params.Username, dev.params.Password)
}
return servResp, err
}
func createHttpRequest(httpMethod string, endpoint string, soap string) (req *http.Request, err error) {
req, err = http.NewRequest(httpMethod, endpoint, bytes.NewBufferString(soap))
if err != nil {
return nil, err
}
req.Header.Set(ContentType, "application/soap+xml; charset=utf-8")
return req, nil
}
func (dev *Device) CallOnvifFunction(serviceName, functionName string, data []byte) (interface{}, error) {
function, err := FunctionByServiceAndFunctionName(serviceName, functionName)
if err != nil {
return nil, err
}
request, err := createRequest(function, data)
if err != nil {
return nil, fmt.Errorf("fail to create '%s' request for the web service '%s', %v", functionName, serviceName, err)
}
endpoint, err := dev.GetEndpointByRequestStruct(request)
if err != nil {
return nil, err
}
requestBody, err := xml.Marshal(request)
if err != nil {
return nil, err
}
xmlRequestBody := string(requestBody)
servResp, err := dev.SendSoap(endpoint, xmlRequestBody)
if err != nil {
return nil, fmt.Errorf("fail to send the '%s' request for the web service '%s', %v", functionName, serviceName, err)
}
defer servResp.Body.Close()
rsp, err := ioutil.ReadAll(servResp.Body)
if err != nil {
return nil, err
}
responseEnvelope, err := createResponse(function, rsp)
if err != nil {
return nil, fmt.Errorf("fail to create '%s' response for the web service '%s', %v", functionName, serviceName, err)
}
if servResp.StatusCode == http.StatusUnauthorized {
return nil, fmt.Errorf("fail to verify the authentication for the function '%s' of web service '%s'. Onvif error: %s",
functionName, serviceName, responseEnvelope.Body.Fault.String())
} else if servResp.StatusCode == http.StatusBadRequest {
return nil, fmt.Errorf("invalid request for the function '%s' of web service '%s'. Onvif error: %s",
functionName, serviceName, responseEnvelope.Body.Fault.String())
} else if servResp.StatusCode > http.StatusNoContent {
return nil, fmt.Errorf("fail to execute the request for the function '%s' of web service '%s'. Onvif error: %s",
functionName, serviceName, responseEnvelope.Body.Fault.String())
}
return responseEnvelope.Body.Content, nil
}
func createRequest(function Function, data []byte) (interface{}, error) {
request := function.Request()
if len(data) > 0 {
err := json.Unmarshal(data, request)
if err != nil {
return nil, err
}
}
return request, nil
}
func createResponse(function Function, data []byte) (*gosoap.SOAPEnvelope, error) {
response := function.Response()
responseEnvelope := gosoap.NewSOAPEnvelope(response)
err := xml.Unmarshal(data, responseEnvelope)
if err != nil {
return nil, err
}
return responseEnvelope, nil
}
// SendGetSnapshotRequest sends the Get request to retrieve the snapshot from the Onvif camera
// The parameter url is come from the "GetSnapshotURI" command.
func (dev *Device) SendGetSnapshotRequest(url string) (resp *http.Response, err error) {
soap := gosoap.NewEmptySOAP()
soap.AddRootNamespaces(Xlmns)
if dev.params.AuthMode == UsernameTokenAuth {
soap.AddWSSecurity(dev.params.Username, dev.params.Password)
var req *http.Request
req, err = createHttpRequest(http.MethodGet, url, soap.String())
if err != nil {
return nil, err
}
// Basic auth might work for some camera
req.SetBasicAuth(dev.params.Username, dev.params.Password)
resp, err = dev.params.HttpClient.Do(req)
} else if dev.params.AuthMode == DigestAuth || dev.params.AuthMode == Both {
soap.AddWSSecurity(dev.params.Username, dev.params.Password)
resp, err = dev.digestClient.Do(http.MethodGet, url, soap.String())
} else {
var req *http.Request
req, err = createHttpRequest(http.MethodGet, url, soap.String())
if err != nil {
return nil, err
}
resp, err = dev.params.HttpClient.Do(req)
}
return resp, err
}