mirror of
https://github.com/kerberos-io/onvif.git
synced 2026-08-23 15:08:33 +00:00
feat: enhance authentication mechanism and improve event handling
- Added EndpointRefAddress to DeviceParams for better endpoint management. - Updated FilterType to use pointers for TopicExpression and MessageContent. - Refactored SendSoapWithDigest to strip WS-Security headers to avoid credential duplication. - Updated package imports to reflect new repository structure. - Introduced ReadAndParse function for improved HTTP response handling in SDK.
This commit is contained in:
282
Device.go
282
Device.go
@@ -94,11 +94,12 @@ type Device struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DeviceParams struct {
|
type DeviceParams struct {
|
||||||
Xaddr string
|
Xaddr string
|
||||||
Username string
|
EndpointRefAddress string
|
||||||
Password string
|
Username string
|
||||||
HttpClient *http.Client
|
Password string
|
||||||
AuthMode string
|
HttpClient *http.Client
|
||||||
|
AuthMode string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetServices return available endpoints
|
// GetServices return available endpoints
|
||||||
@@ -106,30 +107,21 @@ func (dev *Device) GetServices() map[string]string {
|
|||||||
return dev.endpoints
|
return dev.endpoints
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDeviceInfo return available endpoints
|
// GetServices return available endpoints
|
||||||
func (dev *Device) GetDeviceInfo() DeviceInfo {
|
func (dev *Device) GetDeviceInfo() DeviceInfo {
|
||||||
return dev.info
|
return dev.info
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDeviceParams return available endpoints
|
// SetDeviceInfoFromScopes goes through the scopes and sets the device info fields for supported categories (currently name and hardware).
|
||||||
func (dev *Device) GetDeviceParams() DeviceParams {
|
// See 7.3.2.2 Scopes in the ONVIF Core Specification (https://www.onvif.org/specs/core/ONVIF-Core-Specification.pdf).
|
||||||
return dev.params
|
func (dev *Device) SetDeviceInfoFromScopes(scopes []string) {
|
||||||
}
|
newInfo := dev.info
|
||||||
|
supportedScopes := []struct {
|
||||||
func readResponse(resp *http.Response) string {
|
category string
|
||||||
b, err := ioutil.ReadAll(resp.Body)
|
setField func(s string)
|
||||||
if err != nil {
|
}{
|
||||||
panic(err)
|
{category: "name", setField: func(s string) { newInfo.Name = s }},
|
||||||
}
|
{category: "hardware", setField: func(s string) { newInfo.Model = s }},
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAvailableDevicesAtSpecificEthernetInterface ...
|
|
||||||
func GetAvailableDevicesAtSpecificEthernetInterface(interfaceName string) ([]Device, error) {
|
|
||||||
// Call a ws-discovery Probe Message to Discover NVT type Devices
|
|
||||||
devices, err := wsdiscovery.SendProbe(interfaceName, nil, []string{"dn:" + NVT.String()}, map[string]string{"dn": "http://www.onvif.org/ver10/network/wsdl"})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range scopes {
|
for _, s := range scopes {
|
||||||
@@ -240,7 +232,7 @@ func (dev *Device) buildMethodSOAP(msg string) (gosoap.SoapMessage, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getEndpoint functions get the target service endpoint in a better way
|
// getEndpoint functions get the target service endpoint in a better way
|
||||||
func (dev Device) getEndpoint(endpoint string) (string, error) {
|
func (dev *Device) getEndpoint(endpoint string) (string, error) {
|
||||||
|
|
||||||
// common condition, endpointMark in map we use this.
|
// common condition, endpointMark in map we use this.
|
||||||
if endpointURL, bFound := dev.endpoints[endpoint]; bFound {
|
if endpointURL, bFound := dev.endpoints[endpoint]; bFound {
|
||||||
@@ -291,20 +283,6 @@ func (dev Device) callMethodDo(endpoint string, method interface{}) (*http.Respo
|
|||||||
return dev.sendSOAP(endpoint, soap)
|
return dev.sendSOAP(endpoint, soap)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authentication modes selectable through DeviceParams.AuthMode.
|
|
||||||
const (
|
|
||||||
// NoAuth disables authentication entirely.
|
|
||||||
NoAuth = "none"
|
|
||||||
// DigestAuth uses HTTP digest only, without a WS-Security header.
|
|
||||||
DigestAuth = "digest"
|
|
||||||
// UsernameTokenAuth uses a WS-Security UsernameToken header only and never
|
|
||||||
// falls back to HTTP digest.
|
|
||||||
UsernameTokenAuth = "usernametoken"
|
|
||||||
// Both adds a WS-Security header (when credentials exist) and additionally
|
|
||||||
// answers an HTTP digest challenge if the device requests one.
|
|
||||||
Both = "both"
|
|
||||||
)
|
|
||||||
|
|
||||||
// sendSOAP dispatches an assembled SOAP message to the endpoint using the
|
// sendSOAP dispatches an assembled SOAP message to the endpoint using the
|
||||||
// authentication mechanism selected through DeviceParams.AuthMode.
|
// authentication mechanism selected through DeviceParams.AuthMode.
|
||||||
//
|
//
|
||||||
@@ -316,8 +294,9 @@ const (
|
|||||||
// - DigestAuth ("digest"): HTTP digest only; no WS-Security header.
|
// - DigestAuth ("digest"): HTTP digest only; no WS-Security header.
|
||||||
// - Both ("both") / unset (""): WS-Security credentials are added (when
|
// - Both ("both") / unset (""): WS-Security credentials are added (when
|
||||||
// available) and HTTP digest is attempted only if the device answers with
|
// available) and HTTP digest is attempted only if the device answers with
|
||||||
// an authentication challenge (HTTP 401 Unauthorized). The WS-Security
|
// an authentication challenge (HTTP 401 Unauthorized). On that digest
|
||||||
// header is never stripped.
|
// retry the WS-Security header is dropped so the credentials are not sent
|
||||||
|
// twice.
|
||||||
func (dev Device) sendSOAP(endpoint string, soap gosoap.SoapMessage) (*http.Response, error) {
|
func (dev Device) sendSOAP(endpoint string, soap gosoap.SoapMessage) (*http.Response, error) {
|
||||||
hasCredentials := dev.params.Username != "" || dev.params.Password != ""
|
hasCredentials := dev.params.Username != "" || dev.params.Password != ""
|
||||||
|
|
||||||
@@ -341,3 +320,222 @@ func (dev Device) sendSOAP(endpoint string, soap gosoap.SoapMessage) (*http.Resp
|
|||||||
return networking.SendSoapWithDigest(dev.params.HttpClient, endpoint, soap.String(), dev.params.Username, dev.params.Password)
|
return networking.SendSoapWithDigest(dev.params.HttpClient, endpoint, soap.String(), dev.params.Username, dev.params.Password)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}*/
|
||||||
|
|
||||||
|
// SendSoap POSTs the given body wrapped in a SOAP envelope.
|
||||||
|
func (dev Device) SendSoap(endpoint string, xmlRequestBody string) (*http.Response, error) {
|
||||||
|
return dev.SendSoapWithOptions(endpoint, xmlRequestBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendSoapWithHeader is SendSoap plus arbitrary inner-Header XML —
|
||||||
|
// needed to echo WS-Addressing ReferenceParameters (with
|
||||||
|
// wsa:IsReferenceParameter="true") back to vendors like AXIS that
|
||||||
|
// identify pull-point subscriptions through them rather than the URL.
|
||||||
|
//
|
||||||
|
// xmlHeaderContent must be well-formed XML representing one or more
|
||||||
|
// SOAP Header child elements (siblings are supported; the spec lets
|
||||||
|
// each reference parameter be its own header block). Malformed or
|
||||||
|
// element-free content errors before any request is made.
|
||||||
|
//
|
||||||
|
// SECURITY: do not pass content sourced from untrusted clients. The
|
||||||
|
// API assumes the caller is authoritative for the envelope. Header
|
||||||
|
// content forwards verbatim — among others, <wsse:Security> overrides
|
||||||
|
// auth, <wsa:Action> overrides intent, <wsa:To>/<wsa:ReplyTo>/
|
||||||
|
// <wsa:FaultTo> redirect responses, <wsa:MessageID> enables replay-
|
||||||
|
// token forgery, and <wsu:Timestamp> bypasses freshness checks.
|
||||||
|
func (dev Device) SendSoapWithHeader(endpoint, xmlRequestBody, xmlHeaderContent string) (*http.Response, error) {
|
||||||
|
return dev.SendSoapWithOptions(endpoint, xmlRequestBody, WithSOAPHeader(xmlHeaderContent))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendSoapOption tweaks a single SendSoapWithOptions call. New options
|
||||||
|
// (per-call timeout, context, custom envelope namespaces, ...) should
|
||||||
|
// be added as WithX constructors here rather than as new method
|
||||||
|
// variants on Device.
|
||||||
|
type SendSoapOption func(*soapConfig)
|
||||||
|
|
||||||
|
type soapConfig struct {
|
||||||
|
headerContent string
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSOAPHeader adds inner-Header XML to the envelope. See
|
||||||
|
// SendSoapWithHeader for the content contract.
|
||||||
|
func WithSOAPHeader(headerContent string) SendSoapOption {
|
||||||
|
return func(c *soapConfig) { c.headerContent = headerContent }
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendSoapWithOptions is the workhorse behind SendSoap and
|
||||||
|
// SendSoapWithHeader; call it directly when you need to combine
|
||||||
|
// options or pass options not surfaced by the convenience wrappers.
|
||||||
|
func (dev Device) SendSoapWithOptions(endpoint, xmlRequestBody string, opts ...SendSoapOption) (*http.Response, error) {
|
||||||
|
var cfg soapConfig
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&cfg)
|
||||||
|
}
|
||||||
|
soap := gosoap.NewEmptySOAP()
|
||||||
|
soap.AddStringBodyContent(xmlRequestBody)
|
||||||
|
soap.AddRootNamespaces(Xlmns)
|
||||||
|
soap.AddAction()
|
||||||
|
if cfg.headerContent != "" {
|
||||||
|
if err := soap.AddStringHeaderContents(cfg.headerContent); err != nil {
|
||||||
|
return nil, fmt.Errorf("add header content: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
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.SendSoapWithOptions(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
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ type EndpointReferenceType struct { //wsa http://www.w3.org/2005/08/addressing/w
|
|||||||
|
|
||||||
// FilterType struct
|
// FilterType struct
|
||||||
type FilterType struct {
|
type FilterType struct {
|
||||||
TopicExpression TopicExpressionType `xml:"wsnt:TopicExpression"`
|
TopicExpression *TopicExpressionType `xml:"wsnt:TopicExpression,omitempty"`
|
||||||
MessageContent *QueryExpressionType `xml:"wsnt:MessageContent"`
|
MessageContent *QueryExpressionType `xml:"wsnt:MessageContent,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EndpointReference alias
|
// EndpointReference alias
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/beevik/etree"
|
"github.com/beevik/etree"
|
||||||
"github.com/icholy/digest"
|
|
||||||
"github.com/juju/errors"
|
"github.com/juju/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,83 +32,24 @@ func SendSoap(httpClient *http.Client, endpoint, message string) (*http.Response
|
|||||||
return resp, nil
|
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 {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Readout body to close the connection
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendSoapWithDigest sends a soap message and, when the device answers with an
|
// SendSoapWithDigest sends a soap message and, when the device answers with an
|
||||||
// HTTP 401 digest challenge, transparently retries the request with the
|
// HTTP 401 digest challenge, transparently retries the request with the
|
||||||
// computed HTTP digest Authorization header.
|
// computed HTTP digest Authorization header.
|
||||||
//
|
//
|
||||||
// The initial request is sent as-is, so any WS-Security header already present
|
// Any wsse:Security header present in the message is stripped before sending:
|
||||||
// in the message is preserved. HTTP digest is only attempted when the device
|
// when a device requires HTTP digest the credentials travel in the
|
||||||
// explicitly requests it, which keeps WS-Security-only cameras working while
|
// Authorization header, so keeping the WS-Security UsernameToken in the body
|
||||||
// also supporting cameras that require HTTP digest for authenticated calls.
|
// would put the credentials on the wire twice. Other SOAP header blocks (for
|
||||||
|
// example WS-Addressing reference parameters) are preserved so vendor-specific
|
||||||
|
// routing keeps working across the retry.
|
||||||
func SendSoapWithDigest(httpClient *http.Client, endpoint, message, username, password string) (*http.Response, error) {
|
func SendSoapWithDigest(httpClient *http.Client, endpoint, message, username, password string) (*http.Response, error) {
|
||||||
if httpClient == nil {
|
if httpClient == nil {
|
||||||
httpClient = new(http.Client)
|
httpClient = new(http.Client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avoid sending the credentials twice (WS-Security + digest) on the retry.
|
||||||
|
message = stripWSSecurityHeader(message)
|
||||||
|
|
||||||
resp, err := httpClient.Post(endpoint, soapContentType, bytes.NewBufferString(message))
|
resp, err := httpClient.Post(endpoint, soapContentType, bytes.NewBufferString(message))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, errors.Annotate(err, "Post")
|
return resp, errors.Annotate(err, "Post")
|
||||||
@@ -149,6 +89,30 @@ func SendSoapWithDigest(httpClient *http.Client, endpoint, message, username, pa
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stripWSSecurityHeader removes the wsse:Security header block from a SOAP
|
||||||
|
// envelope, leaving all other header blocks intact. The message is returned
|
||||||
|
// unchanged if it cannot be parsed as XML or has no such header.
|
||||||
|
func stripWSSecurityHeader(message string) string {
|
||||||
|
doc := etree.NewDocument()
|
||||||
|
if err := doc.ReadFromString(message); err != nil {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
security := doc.FindElement("./Envelope/Header/Security")
|
||||||
|
if security == nil {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
header := doc.Root().SelectElement("Header")
|
||||||
|
if header == nil {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
header.RemoveChild(security)
|
||||||
|
data, err := doc.WriteToString()
|
||||||
|
if err != nil {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
var digestParamRe = regexp.MustCompile(`(\w+)=(?:"([^"]*)"|([^,]+))`)
|
var digestParamRe = regexp.MustCompile(`(\w+)=(?:"([^"]*)"|([^,]+))`)
|
||||||
|
|
||||||
// parseDigestChallenge parses the parameters of a WWW-Authenticate: Digest header.
|
// parseDigestChallenge parses the parameters of a WWW-Authenticate: Digest header.
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ package event
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/juju/errors"
|
"github.com/juju/errors"
|
||||||
"github.com/use-go/onvif"
|
"github.com/kerberos-io/onvif"
|
||||||
"github.com/use-go/onvif/sdk"
|
"github.com/kerberos-io/onvif/event"
|
||||||
"github.com/use-go/onvif/event"
|
"github.com/kerberos-io/onvif/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Call_PullMessages forwards the call to dev.CallMethod() then parses the payload of the reply as a PullMessagesResponse.
|
// Call_PullMessages forwards the call to dev.CallMethod() then parses the payload of the reply as a PullMessagesResponse.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package event
|
package event
|
||||||
|
|
||||||
//go:generate go run github.com/use-go/onvif/sdk/codegen event event CreatePullPointSubscription
|
//go:generate go run github.com/kerberos-io/onvif/sdk/codegen event event CreatePullPointSubscription
|
||||||
//go:generate go run github.com/use-go/onvif/sdk/codegen event event GetEventProperties
|
//go:generate go run github.com/kerberos-io/onvif/sdk/codegen event event GetEventProperties
|
||||||
//go:generate go run github.com/use-go/onvif/sdk/codegen event event GetServiceCapabilities
|
//go:generate go run github.com/kerberos-io/onvif/sdk/codegen event event GetServiceCapabilities
|
||||||
//go:generate go run github.com/use-go/onvif/sdk/codegen event event Subscribe
|
//go:generate go run github.com/kerberos-io/onvif/sdk/codegen event event Subscribe
|
||||||
//go:generate go run github.com/use-go/onvif/sdk/codegen event event Unsubscribe
|
//go:generate go run github.com/kerberos-io/onvif/sdk/codegen event event Unsubscribe
|
||||||
//go:generate go run github.com/use-go/onvif/sdk/codegen event event PullMessages
|
//go:generate go run github.com/kerberos-io/onvif/sdk/codegen event event PullMessages
|
||||||
|
|||||||
25
sdk/sdk.go
Normal file
25
sdk/sdk.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/juju/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadAndParse reads the body of the given HTTP reply and unmarshals it into
|
||||||
|
// reply. The tag identifies the ONVIF action for diagnostic purposes.
|
||||||
|
func ReadAndParse(ctx context.Context, httpReply *http.Response, reply interface{}, tag string) error {
|
||||||
|
// TODO(jfsmig): extract the deadline from ctx.Deadline() and apply it on the reply reading
|
||||||
|
b, err := io.ReadAll(httpReply.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Annotate(err, "read")
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReply.Body.Close()
|
||||||
|
|
||||||
|
err = xml.Unmarshal(b, reply)
|
||||||
|
return errors.Annotate(err, "decode")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user