diff --git a/README.md b/README.md index 0b57d41..e523f89 100644 --- a/README.md +++ b/README.md @@ -28,16 +28,33 @@ gonvif completion bash ## Client Usage ```golang -import "github.com/hooklift/gowsdl/soap" - -... - -client := soap.NewClient("http://IP[:PORT]/onvif/Media2") -client.SetHeaders(soap.NewSecurity("USERNAME", "PASSWORD")) -media := wsdl.NewMedia2(client) -resp, err := media.GetProfiles(&wsdl.GetProfiles{ - Type: []string{"All"}, -}) +import ( + "log" + + "github.com/eyetowers/gonvif/pkg/client" +) + +func main() { + // Connect to the Onvif device. + onvif, err := client.New("http://IP[:PORT]", "USERNAME", "PASSWORD") + if err != nil { + log.Fatal(err) + } + // Get the Media2 service client. + media, err := onvif.Media2() + if err != nil { + log.Fatal(err) + } + // Make a request. + resp, err := media.GetProfiles(&wsdl.GetProfiles{ + Type: []string{"All"}, + }) + if err != nil { + log.Fatal(err) + } + // Process the response. + log.Printf("Got profiles: %v", resp) +} ``` ## License diff --git a/cmd/gonvif/device/cmd.go b/cmd/gonvif/device/cmd.go index a9c35a3..d4f1d2a 100644 --- a/cmd/gonvif/device/cmd.go +++ b/cmd/gonvif/device/cmd.go @@ -4,6 +4,7 @@ import ( "github.com/spf13/cobra" "github.com/eyetowers/gonvif/cmd/gonvif/root" + "github.com/eyetowers/gonvif/pkg/client" "github.com/eyetowers/gonvif/pkg/generated/onvif/www_onvif_org/ver10/device/wsdl" ) @@ -21,9 +22,9 @@ func init() { } func ServiceClient(url, username, password string, verbose bool) (wsdl.Device, error) { - serviceURL, err := root.ServiceURL(url, "onvif/device_service") + onvif, err := client.New(url, username, password, verbose) if err != nil { return nil, err } - return wsdl.NewDevice(root.AuthorizedSOAPClient(serviceURL, username, password, verbose)), nil + return onvif.Device() } diff --git a/cmd/gonvif/imaging/cmd.go b/cmd/gonvif/imaging/cmd.go index 98c3228..57b5650 100644 --- a/cmd/gonvif/imaging/cmd.go +++ b/cmd/gonvif/imaging/cmd.go @@ -4,6 +4,7 @@ import ( "github.com/spf13/cobra" "github.com/eyetowers/gonvif/cmd/gonvif/root" + "github.com/eyetowers/gonvif/pkg/client" "github.com/eyetowers/gonvif/pkg/generated/onvif/www_onvif_org/ver20/imaging/wsdl" ) @@ -26,10 +27,10 @@ func init() { ) } -func ServiceClient(url, username, password string, vebose bool) (wsdl.ImagingPort, error) { - serviceURL, err := root.ServiceURL(url, "onvif/Imaging") +func ServiceClient(url, username, password string, verbose bool) (wsdl.ImagingPort, error) { + onvif, err := client.New(url, username, password, verbose) if err != nil { return nil, err } - return wsdl.NewImagingPort(root.AuthorizedSOAPClient(serviceURL, username, password, vebose)), nil + return onvif.Imaging() } diff --git a/cmd/gonvif/media/cmd.go b/cmd/gonvif/media/cmd.go index b0d7cf9..8b1a4b0 100644 --- a/cmd/gonvif/media/cmd.go +++ b/cmd/gonvif/media/cmd.go @@ -4,6 +4,7 @@ import ( "github.com/spf13/cobra" "github.com/eyetowers/gonvif/cmd/gonvif/root" + "github.com/eyetowers/gonvif/pkg/client" "github.com/eyetowers/gonvif/pkg/generated/onvif/www_onvif_org/ver20/media/wsdl" ) @@ -23,9 +24,9 @@ func init() { } func ServiceClient(url, username, password string, verbose bool) (wsdl.Media2, error) { - serviceURL, err := root.ServiceURL(url, "onvif/Media2") + onvif, err := client.New(url, username, password, verbose) if err != nil { return nil, err } - return wsdl.NewMedia2(root.AuthorizedSOAPClient(serviceURL, username, password, verbose)), nil + return onvif.Media2() } diff --git a/cmd/gonvif/ptz/cmd.go b/cmd/gonvif/ptz/cmd.go index aa301e8..87cba30 100644 --- a/cmd/gonvif/ptz/cmd.go +++ b/cmd/gonvif/ptz/cmd.go @@ -4,6 +4,7 @@ import ( "github.com/spf13/cobra" "github.com/eyetowers/gonvif/cmd/gonvif/root" + "github.com/eyetowers/gonvif/pkg/client" "github.com/eyetowers/gonvif/pkg/generated/onvif/www_onvif_org/ver20/ptz/wsdl" ) @@ -22,9 +23,9 @@ func init() { } func ServiceClient(url, username, password string, verbose bool) (wsdl.PTZ, error) { - u, err := root.ServiceURL(url, "onvif/PTZ") + onvif, err := client.New(url, username, password, verbose) if err != nil { return nil, err } - return wsdl.NewPTZ(root.AuthorizedSOAPClient(u, username, password, verbose)), nil + return onvif.PTZ() } diff --git a/cmd/gonvif/root/cmd.go b/cmd/gonvif/root/cmd.go index ec0e1a6..82fabf6 100644 --- a/cmd/gonvif/root/cmd.go +++ b/cmd/gonvif/root/cmd.go @@ -1,17 +1,9 @@ package root import ( - "bytes" "encoding/json" - "fmt" - "io" - "log" - "net/http" - "net/url" "os" - "github.com/hooklift/gowsdl/soap" - "github.com/motemen/go-loghttp" "github.com/spf13/cobra" ) @@ -41,52 +33,9 @@ func RequireAuthFlags(cmd *cobra.Command) { cmd.MarkPersistentFlagRequired("password") } -func ServiceURL(baseURL, suffix string) (string, error) { - base, err := url.Parse(baseURL) - if err != nil { - return "", fmt.Errorf("malformed base URL: %w", err) - } - u, err := url.Parse(suffix) - if err != nil { - return "", fmt.Errorf("malformed service suffix URL: %w", err) - } - return base.ResolveReference(u).String(), nil -} - -func AuthorizedSOAPClient(serviceURL, username, password string, verbose bool) *soap.Client { - httpClient := http.DefaultClient - if verbose { - httpClient = &http.Client{ - Transport: &loghttp.Transport{ - LogResponse: logResponse, - LogRequest: logRequest, - }, - } - } - client := soap.NewClient(serviceURL, soap.WithHTTPClient(httpClient)) - client.SetHeaders(soap.NewSecurity(username, password)) - return client -} - func OutputJSON(payload interface{}) error { encoder := json.NewEncoder(os.Stdout) encoder.SetIndent("", " ") return encoder.Encode(payload) } - -func logResponse(resp *http.Response) { - log.Printf("<-- %d %s", resp.StatusCode, resp.Request.URL) - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - log.Printf("BODY:\n%s", string(body)) - resp.Body = io.NopCloser(bytes.NewReader(body)) -} - -func logRequest(req *http.Request) { - log.Printf("--> %s %s", req.Method, req.URL) - defer req.Body.Close() - body, _ := io.ReadAll(req.Body) - log.Printf("BODY:\n%s", string(body)) - req.Body = io.NopCloser(bytes.NewReader(body)) -} diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 0000000..ee95e1d --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1,172 @@ +package client + +import ( + "bytes" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + + device "github.com/eyetowers/gonvif/pkg/generated/onvif/www_onvif_org/ver10/device/wsdl" + analytics "github.com/eyetowers/gonvif/pkg/generated/onvif/www_onvif_org/ver20/analytics/wsdl" + imaging "github.com/eyetowers/gonvif/pkg/generated/onvif/www_onvif_org/ver20/imaging/wsdl" + media2 "github.com/eyetowers/gonvif/pkg/generated/onvif/www_onvif_org/ver20/media/wsdl" + ptz "github.com/eyetowers/gonvif/pkg/generated/onvif/www_onvif_org/ver20/ptz/wsdl" + "github.com/hooklift/gowsdl/soap" + "github.com/motemen/go-loghttp" +) + +var ( + ErrServiceNotSupported = errors.New("onvif service not supported") + + verboseHTTPClient = &http.Client{ + Transport: &loghttp.Transport{ + LogResponse: logResponse, + LogRequest: logRequest, + }, + } +) + +type Client interface { + Analytics() (analytics.AnalyticsEnginePort, error) + Device() (device.Device, error) + Imaging() (imaging.ImagingPort, error) + Media2() (media2.Media2, error) + PTZ() (ptz.PTZ, error) +} + +type impl struct { + analytics analytics.AnalyticsEnginePort + device device.Device + imaging imaging.ImagingPort + media2 media2.Media2 + ptz ptz.PTZ +} + +func New(baseURL, username, password string, verbose bool) (Client, error) { + soapClient, err := serviceSOAPClient(baseURL, "onvif/device_service", username, password, verbose) + if err != nil { + return nil, err + } + d := device.NewDevice(soapClient) + resp, err := d.GetServices(&device.GetServices{}) + if err != nil { + return nil, fmt.Errorf("listing available Onvif services: %w", err) + } + + var result impl + for _, svc := range resp.Service { + svcClient, err := serviceSOAPClient(baseURL, svc.XAddr, username, password, verbose) + if err != nil { + return nil, err + } + if svc.Namespace == "http://www.onvif.org/ver20/analytics/wsdl" { + result.analytics = analytics.NewAnalyticsEnginePort(svcClient) + } + if svc.Namespace == "http://www.onvif.org/ver10/device/wsdl" { + result.device = device.NewDevice(svcClient) + } + if svc.Namespace == "http://www.onvif.org/ver20/imaging/wsdl" { + result.imaging = imaging.NewImagingPort(svcClient) + } + if svc.Namespace == "http://www.onvif.org/ver20/media/wsdl" { + result.media2 = media2.NewMedia2(svcClient) + } + if svc.Namespace == "http://www.onvif.org/ver20/ptz/wsdl" { + result.ptz = ptz.NewPTZ(svcClient) + } + } + + return &result, nil +} + +func (c *impl) Analytics() (analytics.AnalyticsEnginePort, error) { + if c.analytics == nil { + return nil, ErrServiceNotSupported + } + return c.analytics, nil +} + +func (c *impl) Device() (device.Device, error) { + if c.device == nil { + return nil, ErrServiceNotSupported + } + return c.device, nil +} + +func (c *impl) Imaging() (imaging.ImagingPort, error) { + if c.imaging == nil { + return nil, ErrServiceNotSupported + } + return c.imaging, nil +} + +func (c *impl) Media2() (media2.Media2, error) { + if c.media2 == nil { + return nil, ErrServiceNotSupported + } + return c.media2, nil +} + +func (c *impl) PTZ() (ptz.PTZ, error) { + if c.ptz == nil { + return nil, ErrServiceNotSupported + } + return c.ptz, nil +} + +func serviceURL(baseURL, suffix string) (string, error) { + base, err := url.Parse(baseURL) + if err != nil { + return "", fmt.Errorf("malformed base URL: %w", err) + } + u, err := url.Parse(suffix) + if err != nil { + return "", fmt.Errorf("malformed service suffix URL: %w", err) + } + return base.ResolveReference(u).String(), nil +} + +func sanitizeServiceURL(baseURL, advertisedURL string) (string, error) { + u, err := url.Parse(advertisedURL) + if err != nil { + return "", fmt.Errorf("malformed service advertised URL: %w", err) + } + return serviceURL(baseURL, u.Path) +} + +func serviceSOAPClient(baseURL, advertisedURL, username, password string, verbose bool) (*soap.Client, error) { + u, err := sanitizeServiceURL(baseURL, advertisedURL) + if err != nil { + return nil, err + } + return AuthorizedSOAPClient(u, username, password, verbose), nil +} + +func AuthorizedSOAPClient(serviceURL, username, password string, verbose bool) *soap.Client { + httpClient := http.DefaultClient + if verbose { + httpClient = verboseHTTPClient + } + client := soap.NewClient(serviceURL, soap.WithHTTPClient(httpClient)) + client.SetHeaders(soap.NewSecurity(username, password)) + return client +} + +func logResponse(resp *http.Response) { + log.Printf("<-- %d %s", resp.StatusCode, resp.Request.URL) + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + log.Printf("BODY:\n%s", string(body)) + resp.Body = io.NopCloser(bytes.NewReader(body)) +} + +func logRequest(req *http.Request) { + log.Printf("--> %s %s", req.Method, req.URL) + defer req.Body.Close() + body, _ := io.ReadAll(req.Body) + log.Printf("BODY:\n%s", string(body)) + req.Body = io.NopCloser(bytes.NewReader(body)) +} diff --git a/pkg/generated/onvif/www_onvif_org/ver10/device/wsdl/0.go b/pkg/generated/onvif/www_onvif_org/ver10/device/wsdl/0.go index f5253d3..580e114 100644 --- a/pkg/generated/onvif/www_onvif_org/ver10/device/wsdl/0.go +++ b/pkg/generated/onvif/www_onvif_org/ver10/device/wsdl/0.go @@ -52,7 +52,7 @@ type GetServices struct { XMLName xml.Name `xml:"http://www.onvif.org/ver10/device/wsdl GetServices" json:"-"` // Indicates if the service capabilities (untyped) should be included in the response. - IncludeCapability bool `xml:"IncludeCapability,omitempty" json:"IncludeCapability,omitempty"` + IncludeCapability bool `xml:"IncludeCapability" json:"IncludeCapability,omitempty"` } type GetServicesResponse struct {