From ee8a91993243c628338a5d204a70da7874e707c7 Mon Sep 17 00:00:00 2001 From: Cedric Verstraeten Date: Wed, 21 Aug 2024 16:07:06 +0200 Subject: [PATCH] support for latest hikivision ONVIF 19.12 --- Device.go | 54 ++++++++++++++++++++++++++--- api/api.go | 4 ++- deviceio/types.go | 4 +-- go.mod | 2 ++ go.sum | 12 +++++-- names.go | 2 +- networking/networking.go | 75 +++++++++++++++++++++++++++++++++++++++- xsd/onvif/onvif.go | 4 +-- 8 files changed, 143 insertions(+), 14 deletions(-) diff --git a/Device.go b/Device.go index ec4c258..fec909e 100644 --- a/Device.go +++ b/Device.go @@ -13,6 +13,7 @@ import ( "strconv" "strings" + "github.com/kerberos-io/onvif/networking" "github.com/kerberos-io/onvif/xsd/onvif" "github.com/beevik/etree" @@ -253,7 +254,7 @@ func (dev *Device) getEndpoint(endpoint string) (string, error) { // CallMethod functions call an method, defined struct. // You should use Authenticate method to call authorized requests. -func (dev *Device) CallMethod(method interface{}) (*http.Response, error) { +func (dev Device) CallMethod(method interface{}) (*http.Response, error) { pkgPath := strings.Split(reflect.TypeOf(method).PkgPath(), "/") pkg := strings.ToLower(pkgPath[len(pkgPath)-1]) @@ -261,11 +262,35 @@ func (dev *Device) CallMethod(method interface{}) (*http.Response, error) { if err != nil { return nil, err } - requestBody, err := xml.Marshal(method) + return dev.callMethodDo(endpoint, method) +} + +// CallMethod functions call an method, defined 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 } - return dev.SendSoap(endpoint, string(requestBody)) + + 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 { + servResp, err = networking.SendSoapWithDigest(new(http.Client), endpoint, soap.String(), dev.params.Username, dev.params.Password) + } + + return servResp, err } func (dev *Device) GetDeviceParams() DeviceParams { @@ -283,7 +308,7 @@ func (dev *Device) GetEndpointByRequestStruct(requestStruct interface{}) (string return endpoint, err } -func (dev *Device) SendSoap(endpoint string, xmlRequestBody string) (resp *http.Response, err error) { +/*func (dev *Device) SendSoap(endpoint string, xmlRequestBody string) (resp *http.Response, err error) { soap := gosoap.NewEmptySOAP() soap.AddStringBodyContent(xmlRequestBody) soap.AddRootNamespaces(Xlmns) @@ -302,6 +327,27 @@ func (dev *Device) SendSoap(endpoint string, xmlRequestBody string) (resp *http. resp, err = dev.params.HttpClient.Do(req) } return resp, err +}*/ + +// CallMethod functions call an method, defined 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 { + servResp, err = networking.SendSoapWithDigest(new(http.Client), 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) { diff --git a/api/api.go b/api/api.go index ca0ac12..bbe50d9 100644 --- a/api/api.go +++ b/api/api.go @@ -122,6 +122,8 @@ func callNecessaryMethod(serviceName, methodName, acceptedData, username, passwo switch strings.ToLower(serviceName) { case "device": methodStruct, err = getDeviceStructByName(methodName) + case "deviceio": + methodStruct, err = getDeviceStructByName(methodName) case "ptz": methodStruct, err = getPTZStructByName(methodName) case "media": @@ -150,7 +152,7 @@ func callNecessaryMethod(serviceName, methodName, acceptedData, username, passwo servResp, err := networking.SendSoap(new(http.Client), endpoint, soap.String()) if err != nil { - return "", err + servResp, err = networking.SendSoapWithDigest(new(http.Client), endpoint, soap.String(), username, password) } rsp, err := ioutil.ReadAll(servResp.Body) diff --git a/deviceio/types.go b/deviceio/types.go index 4369589..9bff659 100644 --- a/deviceio/types.go +++ b/deviceio/types.go @@ -680,7 +680,7 @@ type GetRelayOutputs struct { } type GetRelayOutputsResponse struct { - RelayOutputs onvif.RelayOutput + RelayOutputs []onvif.RelayOutput } type GetDigitalInputs struct { @@ -688,7 +688,7 @@ type GetDigitalInputs struct { } type GetDigitalInputsResponse struct { - DigitalInputs onvif.DigitalInput + DigitalInputs []onvif.DigitalInput } type SetRelayOutputSettings struct { diff --git a/go.mod b/go.mod index f981751..f98d94f 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,8 @@ require ( github.com/elgs/gostrgen v0.0.0-20161222160715-9d61ae07eeae github.com/gin-gonic/gin v1.9.1 github.com/google/uuid v1.4.0 + github.com/icholy/digest v0.1.23 + github.com/juju/errors v1.0.0 github.com/stretchr/testify v1.8.4 golang.org/x/net v0.19.0 ) diff --git a/go.sum b/go.sum index 5207087..b3fac64 100644 --- a/go.sum +++ b/go.sum @@ -29,16 +29,22 @@ github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QX github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/icholy/digest v0.1.23 h1:4hX2pIloP0aDx7RJW0JewhPPy3R8kU+vWKdxPsCCGtY= +github.com/icholy/digest v0.1.23/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM= +github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= @@ -81,14 +87,14 @@ golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/names.go b/names.go index 4a31c8e..5b9cb22 100644 --- a/names.go +++ b/names.go @@ -75,7 +75,7 @@ const ( GetNetworkProtocols = "GetNetworkProtocols" GetPkcs10Request = "GetPkcs10Request" GetRelayOutputs = "GetRelayOutputs" - GetDigitalInputs = "GetDigitalInputs" + GetDigitalInputs = "GetDigitalInputs" GetRemoteDiscoveryMode = "GetRemoteDiscoveryMode" GetRemoteUser = "GetRemoteUser" GetScopes = "GetScopes" diff --git a/networking/networking.go b/networking/networking.go index 7e1aef2..9903be4 100644 --- a/networking/networking.go +++ b/networking/networking.go @@ -2,14 +2,87 @@ package networking import ( "bytes" + "fmt" + "io" "net/http" + + "github.com/beevik/etree" + "github.com/icholy/digest" + "github.com/juju/errors" ) // SendSoap send soap message func SendSoap(httpClient *http.Client, endpoint, message string) (*http.Response, error) { resp, err := httpClient.Post(endpoint, "application/soap+xml; charset=utf-8", bytes.NewBufferString(message)) if err != nil { - return resp, err + return resp, errors.Annotate(err, "Post") + } + + // if resp.StatusCode is 4xx,5xx, return error + if resp.StatusCode >= 400 && resp.StatusCode < 600 { + return resp, errors.Errorf("Server error: %d: %s", resp.StatusCode, resp.Status) + } + + return resp, nil +} + +func SendSoapWithDigest(httpClient *http.Client, endpoint, message, username, password string) (*http.Response, error) { + doc := etree.NewDocument() + if err := doc.ReadFromString(message); err != nil { + return nil, err + } + + e := doc.FindElement("./Envelope/Header/Security") + if e != nil { + bodyTag := doc.Root().SelectElement("Header") + bodyTag.RemoveChild(e) + data, err := doc.WriteToString() + if err != nil { + return nil, err + } + message = data + } + + req, err := http.NewRequest("POST", endpoint, bytes.NewBufferString(message)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/soap+xml; charset=utf-8") + resp, err := httpClient.Do(req) + if err != nil { + fmt.Println(err) + return resp, errors.Annotate(err, "Post with digest") + } + + if resp.StatusCode != http.StatusUnauthorized { + return resp, err + } + + wwwAuth := resp.Header.Get("WWW-Authenticate") + chal, err := digest.ParseChallenge(wwwAuth) + if err != nil { + return resp, fmt.Errorf("fail to parse challenge: %w", err) + } + + cred, err := digest.Digest(chal, digest.Options{ + Method: "POST", + URI: req.URL.RequestURI(), + Username: username, + Password: password, + }) + + if err != nil { + return resp, fmt.Errorf("fail to build digest: %w", err) + } + + req.Header.Add("Authorization", cred.String()) + req.Body = io.NopCloser((bytes.NewBufferString(message))) + resp, err = httpClient.Do(req) + if err != nil { + return nil, errors.Annotate(err, "Post with digest") + } + if resp.StatusCode >= 400 && resp.StatusCode < 600 { + return resp, errors.Errorf("Post with digest error: %d: %s", resp.StatusCode, resp.Status) } return resp, nil diff --git a/xsd/onvif/onvif.go b/xsd/onvif/onvif.go index adc48dd..4c8106b 100644 --- a/xsd/onvif/onvif.go +++ b/xsd/onvif/onvif.go @@ -1840,8 +1840,8 @@ type RelayOutputSettings struct { } type DigitalInput struct { - Token ReferenceToken `xml:"token,attr"` - IdleState InputIdleState `xml:"IdleState,attr"` + Token ReferenceToken `xml:"token,attr"` + IdleState InputIdleState `xml:"IdleState,attr"` } // TODO:enumeration