mirror of
https://github.com/kerberos-io/onvif.git
synced 2025-10-05 07:46:49 +08:00
471 lines
14 KiB
Go
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
|
|
}
|