Skip to content

Commit

Permalink
support for latest hikivision ONVIF 19.12
Browse files Browse the repository at this point in the history
  • Loading branch information
cedricve committed Aug 21, 2024
1 parent d1b78fa commit ee8a919
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 13 deletions.
54 changes: 50 additions & 4 deletions Device.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"strconv"
"strings"

"github.com/kerberos-io/onvif/networking"
"github.com/kerberos-io/onvif/xsd/onvif"

"github.com/beevik/etree"
Expand Down Expand Up @@ -253,19 +254,43 @@ func (dev *Device) getEndpoint(endpoint string) (string, error) {

// 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) {
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
}
requestBody, err := xml.Marshal(method)
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
}
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 {
Expand All @@ -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)
Expand All @@ -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 <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 {
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) {
Expand Down
4 changes: 3 additions & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions deviceio/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -680,15 +680,15 @@ type GetRelayOutputs struct {
}

type GetRelayOutputsResponse struct {
RelayOutputs onvif.RelayOutput
RelayOutputs []onvif.RelayOutput
}

type GetDigitalInputs struct {
XMLName string `xml:"tmd:GetDigitalInputs"`
}

type GetDigitalInputsResponse struct {
DigitalInputs onvif.DigitalInput
DigitalInputs []onvif.DigitalInput
}

type SetRelayOutputSettings struct {
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
12 changes: 9 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
2 changes: 1 addition & 1 deletion names.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 73 additions & 0 deletions networking/networking.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,88 @@ 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, 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
}
4 changes: 2 additions & 2 deletions xsd/onvif/onvif.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit ee8a919

Please sign in to comment.