ONVIF example ============= Overview -------- ONVIF [Open Network Video Interface Forum](http://onvif.org) is a global and open industry forum with the goal of facilitating the development and use of a global open standard for the interface of physical IP-based security products. The standard defines communication protocols for IP products within video surveillance and other physical security areas. The [ONVIF specifications](https://www.onvif.org/profiles/specifications/) are available as WSDL (Web Services Description Language) files. gSOAP consumes these WSDL files and automatically converts them to C/C++ source code to implement ONVIF protocol message exchanges in XML. This frees the developer to focus on application functionality rather than on infrastructure. This article first describes the common steps to implement ONVIF applications. We then describe an example C++ application to take a snapshot from an ONVIF compliant IP camera using a client application written in C++. We recommend gSOAP 2.8.62 or greater for ONVIF projects. [![To top](../../images/go-up.png) To top](#) Converting ONVIF WSDLs to C/C++ ------------------------------- The gSOAP wsdl2h tool consumes WSDLs to generate a C or C++ interface file, which uses a developer-friendly C/C++ header file syntax. This allows you to inspect the ONVIF services from a functionality point of view, rather than the underlying SOAP-based infrastructure details. The wsdl2h tool needs a `typemap.dat` file to map XSD schema types to C/C++ types and to bind XML namespace prefixes to short names that you want to use in your project. The `typemap.dat` file is included with the gSOAP source code downloads. This file defines the following XML namespace prefixes and data types among other things: [command] # ONVIF recommended prefixes tds = "http://www.onvif.org/ver10/device/wsdl" tev = "http://www.onvif.org/ver10/events/wsdl" tls = "http://www.onvif.org/ver10/display/wsdl" tmd = "http://www.onvif.org/ver10/deviceIO/wsdl" timg = "http://www.onvif.org/ver20/imaging/wsdl" trt = "http://www.onvif.org/ver10/media/wsdl" tptz = "http://www.onvif.org/ver20/ptz/wsdl" trv = "http://www.onvif.org/ver10/receiver/wsdl" trc = "http://www.onvif.org/ver10/recording/wsdl" tse = "http://www.onvif.org/ver10/search/wsdl" trp = "http://www.onvif.org/ver10/replay/wsdl" tan = "http://www.onvif.org/ver20/analytics/wsdl" tad = "http://www.onvif.org/ver10/analyticsdevice/wsdl" tas = "http://www.onvif.org/ver10/advancedsecurity/wsdl" tdn = "http://www.onvif.org/ver10/network/wsdl" tt = "http://www.onvif.org/ver10/schema" # OASIS recommended prefixes wsnt = "http://docs.oasis-open.org/wsn/b-2" wsntw = "http://docs.oasis-open.org/wsn/bw-2" wsrfbf = "http://docs.oasis-open.org/wsrf/bf-2" wsrfr = "http://docs.oasis-open.org/wsrf/r-2" wsrfrw = "http://docs.oasis-open.org/wsrf/rw-2" wstop = "http://docs.oasis-open.org/wsn/t-1" # WS-Discovery 1.0 remapping wsdd5__HelloType = | wsdd__HelloType wsdd5__ByeType = | wsdd__ByeType wsdd5__ProbeType = | wsdd__ProbeType wsdd5__ProbeMatchesType = | wsdd__ProbeMatchesType wsdd5__ProbeMatchType = | wsdd__ProbeMatchType wsdd5__ResolveType = | wsdd__ResolveType wsdd5__ResolveMatchesType = | wsdd__ResolveMatchesType wsdd5__ResolveMatchType = | wsdd__ResolveMatchType wsdd5__ScopesType = | wsdd__ScopesType wsdd5__SecurityType = | wsdd__SecurityType wsdd5__SigType = | wsdd__SigType wsdd5__AppSequenceType = | wsdd__AppSequenceType # SOAP-ENV mapping SOAP_ENV__Envelope = struct SOAP_ENV__Envelope { struct SOAP_ENV__Header *SOAP_ENV__Header; _XML SOAP_ENV__Body; }; | struct SOAP_ENV__Envelope SOAP_ENV__Header = | struct SOAP_ENV__Header SOAP_ENV__Fault = | struct SOAP_ENV__Fault SOAP_ENV__Detail = | struct SOAP_ENV__Detail SOAP_ENV__Code = | struct SOAP_ENV__Code SOAP_ENV__Subcode = | struct SOAP_ENV__Subcode SOAP_ENV__Reason = | struct SOAP_ENV__Reason Without the XML namespace prefix associations specified here as shown at the top, wsdl2h will just generate `ns1`, `ns2` and so on, which can be confusing when you have to deal with several namespaces in your project. In fact, the ONVIF specifications are evolving, so if new WSDLs and XSDs are added to the ONVIF specifications by the ONVIF organization you may want to add XML namespace prefix associations for these to this `typemap.dat` file. Just run wsdl2h as shown below and inspect the generated `onvif.h` file to see if any `ns1`, `ns2` prefixes show up that require a binding (as explained in the generated `onvif.h` file). Make sure to place `typemap.dat` in the current directory where you want to run wsdl2h. Then at the command line prompt, run wsdl2h on the URLs of all of the ONVIF WSDLs that you want to convert to C/C++ code. For example, the device management WSDL is converted as follows: [command] wsdl2h -O4 -P -x -o onvif.h http://www.onvif.org/onvif/ver10/device/wsdl/devicemgmt.wsdl where we used the following options: - `-O4` aggressively optimizes the output by "schema slicing" to remove unused schema components, see our article [Schema Slicing Methods to Reduce Development Costs of WSDL-Based Web Services](../../slicing.html) for details; - `-P` removes the base class `xsd__anyType` from the generated C++ classes, which are normally added by wsdl2h if the *`xsd:anyType`* XSD type is used somewhere in a WSDL. However, for the ONVIF protocols we do not need to inherit the `xsd__anyType` class and we can reduce the generated code size accordingly; - `-x` removes the unnecessary generated code for the extensibility elements *`xsd:any`* and attributes *`xsd:anyAttribue`*, since we do not need to support these in general, except for some specific cases see further below; - `-o onvif.h` saves the binding interface code to the header file `onvif.h`; - Optionally, use `-c` to generate C source code instead of C++ source code. The `onvif.h` file contains all the details pertaining the ONVIF services in a developer-friendly format. It specifies the ONVIF data types used and the ONVIF Web services operations available. Now we are ready to run soapcpp2 on the generated `onvif.h` binding interface to generate the Web services binding source code: [command] soapcpp2 -2 -I ~/gsoap-2.8/gsoap/import onvif.h where we used the following options: - `-2` forces SOAP 1.2, which is required by ONVIF; - `-I` sets the import path to the `~/gsoap-2.8/gsoap/import` directory in the gSOAP source code tree, note that this is an example and you should specify the actual location of the gSOAP directory on your system. For C++ projects you should generate C++ proxy classes using soapcpp2 option `-j`. Proxy classes are easier to use than the global functions that are generated without this option. See the example further below. Note: this is unlikely, but if it appears that multiple service operations are generated with the same name, such as `GetServices` and `GetServices_` then remove some of the WSDLs (such as `http://www.onvif.org/onvif/ver10/device/wsdl/devicemgmt.wsdl`) from the wsdl2h command, because these WSDLs are already imported by one or more of the other WSDLs you have specified. The wsdl2h tool checks that WSDLs are read just once when imported by other WSDLs, but when you explicitly specify a WSDL with the wsdl2h command then that WSDL will be read even when it is already imported and converted. Check the printed output of the wsdl2h command that shows the WSDLs being read and converted. [![To top](../../images/go-up.png) To top](#) Smart Serialization of xsd:duration ----------------------------------- The default type mapping by wsdl2h generates a string type for the *`xsd:duration`* XSD type, which means that the string must conform to this XSD type and you will need to make sure your application logic conforms to this standard. A smarter way to use *`xsd:duration`* is to bind it to one of the two choices of custom serializers to automatically handle the *`xsd:duration`* values properly. These two custom serializers are: - `custom/duration.h` serializes *`xsd:duration`* as a 64 bit integer with milliseconds precision, which works both in C and in C++; - `custom/chrono_duration.h` serializes *`xsd:duration`* as a C++11 `std::chrono::nanoseconds` type with nanoseconds precision. To automatically enable the first choice of serializer for *`xsd:duration`*, add the following line to `typemap.dat` if not already there: [command] xsd__duration = #import "custom/duration.h" | xsd__duration This imports `custom/duration.h` into the wsdl2h-generated `onvif.h` binding interface. The `custom` directory is located in the gSOAP source code tree `~/gsoap-2.8/gsoap/custom`. To automatically enable the second choice of serializer for *`xsd:duration`*, add the following line to `typemap.dat` if not already there: [command] xsd__duration = #import "custom/chrono_duration.h" | xsd__duration When compiling your project you must compile `custom/duration.c` for the first choice of serializer or you must compile `custom/chrono_duration.cpp` for the second choice of serializer. [![To top](../../images/go-up.png) To top](#) ONVIF Extensibility ------------------- ONVIF data types may be extensible, permitting additional elements and/or attributes. However, most scenarios associated with profiles do not require these extensions so we remove them by default to reduce the size of the generated source code. Option `-x` of wsdl2h removes all extensibility elements and attributes from the generated class and struct declarations. If specific element and/or attribute extensions to a complexType are still required in specific cases, then we simply use the `typemap.dat` file to add these as additional optional members to the generated classes/structs. For example, to extend `tt__AudioDecoderConfigurationOptionsExtension` complexType with an element `Address` of type `tt:IPAddress` we add the following line to `typemap.dat`: [command] tt__AudioDecoderConfigurationOptionsExtension = $ tt__IPAddress *Address; After running wsdl2h we see that `tt__IPAddress *Address` is now part of the `tt__AudioDecoderConfigurationOptionsExtension` class or struct. A pointer is used to make this element optional and it will not be serialized when set to NULL. [![To top](../../images/go-up.png) To top](#) Using WS-Security ----------------- Most ONVIF services use WS-Security to timestamp messages and for authentication. WS-Security is included with gSOAP and is easy to use as we will explain with the following steps. To compile your gSOAP application with WS-Security follow these steps: - include `gsoap/plugin/wsseapi.h` in your source code to import the gSOAP WS-Security API; - compile `gsoap/plugin/wsseapi.c` with your application to use the gSOAP WS-Security API; - compile `gsoap/plugin/smdevp.c` and `gsoap/plugin/mecevp.c` with your application; - compile all your source code with `-DWITH_OPENSSL` and `-DWITH_DOM` to enable OpenSSL and DOM processing (DOM processessing is required to optionally verify WS-Security digitally-signed messages); - link with `-lgsoapssl` (for C) or `-lgsoapssl++` (for C++), or alternatively to using these libraries, compile `gsoap/stdsoap.c` and `gsoap/dom.c` (for C) or `gsoap/stdsoap2.cpp` and `gsoap/dom.cpp` (for C++); - link OpenSSL with your application by including the libraries `-lcrypto` and `-lssl` in your build. To add a timestamp to a message sent to the ONVIF service, add the following line of code before invoking the client-side (proxy) service operation: soap_wsse_add_Timestamp(soap, "Time", 10); This produces a WS-Security *`wsu:Timestamp`* header that indicates that the message expires in 10 seconds after creation: [xml] <SOAP-ENV:Envelope ...> <SOAP-ENV:Header> <wsse:Security SOAP-ENV:mustUnderstand="true"> <wsu:Timestamp wsu:Id="Time"> <wsu:Created>2018-08-24T10:48:41Z</wsu:Created> <wsu:Expires>2018-08-24T10:48:51Z</wsu:Expires> </wsu:Timestamp> ... To add a WS-Security *`wsse:UsernameToken`* digest password header to a message sent to the ONVIF service, add the following line of code before invoking the client-side (proxy) service operation: soap_wsse_add_UsernameTokenDigest(soap, "Auth", "username", "password"); This produces a WS-Security *`wsse:UsernameToken`* header with the username and a password digest credentials as required to authenticate the request with the ONVIF service: [xml] <SOAP-ENV:Envelope ...> <SOAP-ENV:Header> <wsse:Security SOAP-ENV:mustUnderstand="true"> ... <wsse:UsernameToken wsu:Id="Auth"> <wsse:Username>username</wsse:Username> <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest"> 4o20FOWpX4g03BgnW0gz6X8/hZ8= </wsse:Password> <wsse:Nonce>X5KBW4qKOtsMg1prngaL2EGXV</wsse:Nonce> <wsu:Created>2018-08-24T10:48:41Z</wsu:Created> </wsse:UsernameToken> ... At the server-side implementation in gSOAP of an ONVIF service, each service operation should verify the timestamp and credentials before processing the request. This is done with the following lines of code (here shown in C, but C++ service objects generated with soapcpp2 option `-j` are similar with the method name `ns__someServiceOperation`): int ns__someServiceOperation(struct soap *soap, ...) { const char *username = soap_wsse_get_Username(soap); const char *password; if (!username) { soap_wsse_delete_Security(soap); // remove old security headers before returning! return soap->error; // no username: return FailedAuthentication (from soap_wsse_get_Username) } password = ...; // lookup password of the username provided if (soap_wsse_verify_Password(soap, password)) { soap_wsse_delete_Security(soap); // remove old security headers before returning! return soap->error; // no username: return FailedAuthentication (from soap_wsse_verify_Password) } ... // process request The `soap_wsse_get_Username` call retrieves the username from the message's WS-Security header and `soap_wsse_verify_Password` verifies the password digest given the plain-text password retrieved from an internal store of passwords with `username` as the key. The password store should be strongly protected. We also recommend to store hashed passwords in the store rather than plain-text passwords for obvious reasons (a practice that has been in place in the UNIX world for decades.) SHA1 can be used to hash passwords and store them. Likewise, the password credential should be hashed by the same hash algorithm (e.g. SHA1) before passing its value to `soap_wsse_add_UsernameTokenDigest`. With `soap_wsse_add_UsernameTokenDigest` we ensure that a digest of the (SHA1 hashed) password is sent, so its value cannot be retrieved by eavesdroppers. Also the nonce associated with the credentials is unique per message. This prevents replay attacks by an attacker who resends the message to the ONVIF service. If our messages are sent in the clear then we do not guard ourselves against message tampering by an attacker in the middle because the integrity of our messages is not protected. One way to protect our messages is to use https to connect to ONVIF services. Another way is to use WS-Security to digitally sign our messages. This protects the timestamp, the credentials and the message body from tampering. To sign messages with WS-Security at the client side requires the public and private keys of the client, where the public key is signed by a third party certificate authority (CA) and included in the client's X509 certificate. The private key is used by the client to sign the message. The service then uses the client's public key embedded with the X509 certificate to verify the signed message, after verifying that the certificate is valid by checking that it was signed by a certificate authority. The files that we need at the client side are: - `client.pem` the client's private key; - `clientcert.pem` client certificate signed by a CA (or self-signed for testing purposes); - `cacert.pem` with the root CA certificate or `cacerts.pem` with all of the current common CA certificates. Examples of these are included with the gSOAP examples located in `gsoap/samples/ssl` in the gSOAP source code tree. See the README.txt located there for instructions on how to generate these. To digitally sign messages at the client side is done as follows: // create a soap context with XML canonicalization enabled struct soap *soap = soap_new1(SOAP_XML_CANONICAL | SOAP_C_UTFSTRING); // then register the WSSE plugin: soap_register_plugin(soap, soap_wsse); ... // read the client private key and certificate file FILE *fd = NULL; EVP_PKEY *privk = NULL; X509 *cert = NULL; if ((fd = fopen("client.pem", "r"))) { privk = PEM_read_PrivateKey(fd, NULL, NULL, (void*)"password"); // use the password that was used to encrypt client.pem fclose(fd); } if (!privk) { fprintf(stderr, "Could not read private key from client.pem\n"); exit(EXIT_FAILURE); } if ((fd = fopen("clientcert.pem", "r"))) { cert = PEM_read_X509(fd, NULL, NULL, NULL); fclose(fd); } if (!cert) { fprintf(stderr, "Could not read certificate from clientcert.pem\n"); exit(EXIT_FAILURE); } // quick way to specify the CA certificate(s) to auto-verify signed service responses soap->cafile = "cacert.pem"; // or set a path to CA files with soap->capath = "dir/to/certs"; // optionally specify CRLs with soap->crlfile = "revoked.pem"; ... // before sending the message we set up the timestamp, credentials and sign these and the message body: soap_wsse_add_Timestamp(soap, "Time", 10); soap_wsse_add_UsernameTokenDigest(soap, "Auth", "username", "password"); soap_wsse_add_BinarySecurityTokenX509(soap, "X509Token", cert); soap_wsse_add_KeyInfo_SecurityTokenReferenceX509(soap, "#X509Token"); soap_wsse_sign_body(soap, SOAP_SMD_SIGN_RSA_SHA256, rsa_privk, 0); soap_wsse_verify_auto(soap, SOAP_SMD_NONE, NULL, 0); ... // make the ONVIF API call here ... // check if the server returned a signed message body if (soap_wsse_verify_body(soap)) exit(EXIT_FAILURE); ... // use the response data here ... // clean up security headers and delete all deserialized response data soap_wsse_delete_Security(soap); soap_wsse_verify_done(soap); soap_destroy(soap); soap_end(soap); ... // clean up keys EVP_PKEY_free(privk); X509_free(cert); ... // the last step is to free the context and remove the plugin if we're not reusing it soap_free(soap); See the gSOAP [WS-Security](../../doc/wsse/html/wsse.html) documentation for more details. An example demo client and server application of WS-Security is included in the source code tree `gsoap/wsse/wssedemo.c`, which shows lots of options to use to sign and/or encrypt messages. [![To top](../../images/go-up.png) To top](#) Using WS-Discovery ------------------ When the ONVIF remotediscovery WSDL is used to build an ONVIF application, WS-Discovery is required. To compile your gSOAP application with WS-Discovery requires the following build steps: - include `gsoap/plugin/wsddapi.h` in your source code; - compile `gsoap/plugin/wsddapi.c` with your ONVIF application. Configuring WS-Discovery with the gSOAP WS-Discovery plugin and using the plugin's API is beyond the scope of this article. Please see the gSOAP [WS-Discovery](../../doc/wsdd/html/wsdd_0.html) documentation for details. [![To top](../../images/go-up.png) To top](#) ONVIF Client Application in C++ to Retrieve Image Snapshots ----------------------------------------------------------- This C++ example demonstrates how the retrieve a snapshot from an ONVIF compliant IP camera. We build this application from several ONVIF WSDLs that provide a wide range of ONVIF service API functions to demonstrate how this is accomplished. However, we will only use a small subset of this ONVIF service API for our example application. Add or remove WSDLs as needed for your ONVIF application profile. After installing gSOAP locally in our home directory `~/gsoap-2.8`, we place [`typemap.dat`](typemap.dat) (click on the link to download) in the current directory of our project and then run wsdl2h with the following URLs of the ONVIF WSDLs: [command] cp ~/gsoap-2.8/gsoap/typemap.dat . wsdl2h -O4 -P -x -o onvif.h \ http://www.onvif.org/onvif/ver10/device/wsdl/devicemgmt.wsdl \ http://www.onvif.org/onvif/ver10/events/wsdl/event.wsdl \ http://www.onvif.org/onvif/ver10/deviceio.wsdl \ http://www.onvif.org/onvif/ver20/imaging/wsdl/imaging.wsdl \ http://www.onvif.org/onvif/ver10/media/wsdl/media.wsdl \ http://www.onvif.org/onvif/ver20/ptz/wsdl/ptz.wsdl \ http://www.onvif.org/onvif/ver10/network/wsdl/remotediscovery.wsdl \ http://www.onvif.org/ver10/advancedsecurity/wsdl/advancedsecurity.wsdl Saving onvif.h ** The gSOAP WSDL/WADL/XSD processor for C and C++, wsdl2h release 2.8.70 ** Copyright (C) 2000-2018 Robert van Engelen, Genivia Inc. ** All Rights Reserved. This product is provided "as is", without any warranty. ** The wsdl2h tool and its generated software are released under the GPL. ** ---------------------------------------------------------------------------- ** A commercial use license is available from Genivia Inc., contact@genivia.com ** ---------------------------------------------------------------------------- Reading type definitions from type map "typemap.dat" Connecting to 'http://www.onvif.org/onvif/ver10/device/wsdl/devicemgmt.wsdl' to retrieve WSDL/WADL or XSD... connected, receiving... Redirected to 'https://www.onvif.org/onvif/ver10/device/wsdl/devicemgmt.wsdl'... [... several other WSDLs and XSDs are read here ...] Warning: 8 service bindings found, but collected as one service (use option -Nname to produce a separate service for each binding) Warning: 2 service bindings found, but collected as one service (use option -Nname to produce a separate service for each binding) Warning: 4 service bindings found, but collected as one service (use option -Nname to produce a separate service for each binding) Optimization (-O4): removed 173 definitions of unused schema components (13.7%) To finalize code generation, execute: > soapcpp2 onvif.h Or to generate C++ proxy and service classes: > soapcpp2 -j onvif.h This generates [`onvif.h`](onvif.h) (click on the link to download). This step requires wsdl2h with https enabled, which is built by default when you run `./configure` and `make` on UNIX/Linux systems. Otherwise run `make -f MakefileManual secure` in the `gsoap/wsdl` directory to build wsdl2h with https enabled. Windows users can download wsdl2h.exe with https enabled from our [download](../../downloads.html) page. Even though ONVIF uses WS-Security for authentication, the WSDLs do not specify WS-Security is used so we explicitly add the following line to [`onvif.h`](onvif.h): #import "wsse.h" We also should change `#import "wsdd10.h"` to `#import "wsdd5.h"` (when applicable as this depends on the gSOAP version you are using), because ONVIF uses WS-Addressing 2005/08 whereas WS-Discovery declared in `wsdd10.h` assumes WS-Addressing 2004/08: #import "wsdd5.h" // replaces wsdd10.h to prevent WS-Addressing definition clashes Now we are ready to generate the C++ proxy class and binding source code for our application: [command] soapcpp2 -2 -C -I ~/gsoap-2.8/gsoap/import -j -x onvif.h where option `-2` forces SOAP 1.2, option `-C` generates client code without service code, option `-j` generates C++ proxy classes and option `-x` omits the generation of sample XML messages (which are a lot!) To support client-side WS-Discovery operations, we run soapcpp2 as follows as documented [here](../../doc/wsdd/html/wsdd_0.html#wsdd_6): [command] soapcpp2 -a -x -L -pwsdd -I ~/gsoap-2.8/gsoap/import ~/gsoap-2.8/gsoap/import/wsdd5.h Our main program [`main.cpp`](main.cpp) (click on the link to download) creates proxy classes to access the ONVIF service API as follows: int main() { // make OpenSSL MT-safe with mutex CRYPTO_thread_setup(); // create a context with strict XML validation and exclusive XML canonicalization for WS-Security enabled struct soap *soap = soap_new1(SOAP_XML_STRICT | SOAP_XML_CANONICAL | SOAP_C_UTFSTRING); soap->connect_timeout = soap->recv_timeout = soap->send_timeout = 10; // 10 sec soap_register_plugin(soap, soap_wsse); // enable https connections with server certificate verification using cacerts.pem if (soap_ssl_client_context(soap, SOAP_SSL_SKIP_HOST_CHECK, NULL, NULL, "cacerts.pem", NULL, NULL)) report_error(soap); // create the proxies to access the ONVIF service API at HOSTNAME DeviceBindingProxy proxyDevice(soap); MediaBindingProxy proxyMedia(soap); // get device info and print proxyDevice.soap_endpoint = HOSTNAME; _tds__GetDeviceInformation GetDeviceInformation; _tds__GetDeviceInformationResponse GetDeviceInformationResponse; set_credentials(soap); if (proxyDevice.GetDeviceInformation(&GetDeviceInformation, GetDeviceInformationResponse)) report_error(soap); check_response(soap); std::cout << "Manufacturer: " << GetDeviceInformationResponse.Manufacturer << std::endl; std::cout << "Model: " << GetDeviceInformationResponse.Model << std::endl; std::cout << "FirmwareVersion: " << GetDeviceInformationResponse.FirmwareVersion << std::endl; std::cout << "SerialNumber: " << GetDeviceInformationResponse.SerialNumber << std::endl; std::cout << "HardwareId: " << GetDeviceInformationResponse.HardwareId << std::endl; // get device capabilities and print media _tds__GetCapabilities GetCapabilities; _tds__GetCapabilitiesResponse GetCapabilitiesResponse; set_credentials(soap); if (proxyDevice.GetCapabilities(&GetCapabilities, GetCapabilitiesResponse)) report_error(soap); check_response(soap); if (!GetCapabilitiesResponse.Capabilities || !GetCapabilitiesResponse.Capabilities->Media) { std::cerr << "Missing device capabilities info" << std::endl; exit(EXIT_FAILURE); } std::cout << "XAddr: " << GetCapabilitiesResponse.Capabilities->Media->XAddr << std::endl; if (GetCapabilitiesResponse.Capabilities->Media->StreamingCapabilities) { if (GetCapabilitiesResponse.Capabilities->Media->StreamingCapabilities->RTPMulticast) std::cout << "RTPMulticast: " << (*GetCapabilitiesResponse.Capabilities->Media->StreamingCapabilities->RTPMulticast ? "yes" : "no") << std::endl; if (GetCapabilitiesResponse.Capabilities->Media->StreamingCapabilities->RTP_USCORETCP) std::cout << "RTP_TCP: " << (*GetCapabilitiesResponse.Capabilities->Media->StreamingCapabilities->RTP_USCORETCP ? "yes" : "no") << std::endl; if (GetCapabilitiesResponse.Capabilities->Media->StreamingCapabilities->RTP_USCORERTSP_USCORETCP) std::cout << "RTP_RTSP_TCP: " << (*GetCapabilitiesResponse.Capabilities->Media->StreamingCapabilities->RTP_USCORERTSP_USCORETCP ? "yes" : "no") << std::endl; } // set the Media proxy endpoint to XAddr proxyMedia.soap_endpoint = GetCapabilitiesResponse.Capabilities->Media->XAddr.c_str(); // get device profiles _trt__GetProfiles GetProfiles; _trt__GetProfilesResponse GetProfilesResponse; set_credentials(soap); if (proxyMedia.GetProfiles(&GetProfiles, GetProfilesResponse)) report_error(soap); check_response(soap); // for each profile get snapshot for (int i = 0; i < GetProfilesResponse.Profiles.size(); ++i) { // get snapshot URI for profile _trt__GetSnapshotUri GetSnapshotUri; _trt__GetSnapshotUriResponse GetSnapshotUriResponse; GetSnapshotUri.ProfileToken = GetProfilesResponse.Profiles[i]->token; set_credentials(soap); if (proxyMedia.GetSnapshotUri(&GetSnapshotUri, GetSnapshotUriResponse)) report_error(soap); check_response(soap); std::cout << "Profile name: " << GetProfilesResponse.Profiles[i]->Name << std::endl; if (GetSnapshotUriResponse.MediaUri) save_snapshot(i, GetSnapshotUriResponse.MediaUri->Uri.c_str()); } // free all deserialized and managed data, we can still reuse the context and proxies after this soap_destroy(soap); soap_end(soap); // free the shared context, proxy classes must terminate as well after this soap_free(soap); // clean up OpenSSL mutex CRYPTO_thread_cleanup(); return 0; } In our `main` function we do the following: - Set up OpenSSL mutex locks, just in case we're developing a multi-threaded application. The `CRYPTO_thread_setup` and `CRYPTO_thread_cleanup` functions are defined in [thread_setup.c](../../files/thread_setup.c). - We create one `soap` context for our proxy instances, with `soap_new1(SOAP_XML_STRICT | SOAP_XML_CANONICAL)` to force strict XML validation and XML normalization where the latter is optional but must be used with WS-Security when that is applicable. - We use `SOAP_C_UTFSTRING` to initialize the `soap` context to store UTF-8 encoded Unicode in 8 bit strings (`char*` and `std::string`) when strings are parsed from or rendered in XML. - To establish https connections we should validate the server's certificate with `soap_ssl_client_context` settings, see the [tutorial](../../tutorials.html#tls). - We create two proxy instances `proxyDevice` and `proxyMedia` that use the `soap` context to manage connections and memory. - Before invoking the proxies we call `set_credentials` for authentication (the source code of this function is shown below). - After invoking the proxies we call `check_response` (the source code of this function is shown below). - `proxyDevice.GetDeviceInformation` returns device information which we then print. - `proxyDevice.GetCapabilities` returns device capabilites which we then print. - `proxyMedia.GetProfiles` returns profiles. We set the endpoint of `proxyMedia` with `proxyMedia.soap_endpoint = GetCapabilitiesResponse.Capabilities->Media->XAddr.c_str()`. - Each profile has a snapshot that we retrieve with `proxyMedia.GetSnapshotUri` after which we download the image with `save_snapshot` (the source code of this function is shown below). To set the credentials we use `set_credentials`, which also uses WS-Security to sign the message when macro `PROTECT` is enabled: void set_credentials(struct soap *soap) { soap_wsse_delete_Security(soap); if (soap_wsse_add_Timestamp(soap, "Time", 10) || soap_wsse_add_UsernameTokenDigest(soap, "Auth", USERNAME, PASSWORD)) report_error(soap); #ifdef PROTECT if (!privk) { FILE *fd = fopen("client.pem"; if (fd) { privk = PEM_read_PrivateKey(fd, NULL, NULL, (void*)"password"); fclose(fd); } if (!privk) { fprintf(stderr, "Could not read private key from client.pem\n"); exit(EXIT_FAILURE); } } if (!cert) { FILE *fd = fopen("clientcert.pem", "r"); if (fd) { cert = PEM_read_X509(fd, NULL, NULL, NULL); fclose(fd); } if (!cert) { fprintf(stderr, "Could not read certificate from clientcert.pem\n"); exit(EXIT_FAILURE); } } if (soap_wsse_add_BinarySecurityTokenX509(soap, "X509Token", cert) || soap_wsse_add_KeyInfo_SecurityTokenReferenceX509(soap, "#X509Token") || soap_wsse_sign_body(soap, SOAP_SMD_SIGN_RSA_SHA256, rsa_privk, 0) || soap_wsse_verify_auto(soap, SOAP_SMD_NONE, NULL, 0)) report_error(soap); #endif } When `PROTECT` is enabled, we should check the server responses with `check_response`: void check_response(struct soap *soap) { #ifdef PROTECT // check if the server returned a signed message body, if not error if (soap_wsse_verify_body(soap)) report_error(soap); soap_wsse_delete_Security(soap); #endif } To retrieve a snapshot image with HTTP GET and save it, we use `save_snapshot`: void save_snapshot(int i, const char *endpoint) { char filename[32]; (SOAP_SNPRINTF_SAFE(filename, 32), "image-%d.jpg", i); FILE *fd = fopen(filename, "wb"); if (!fd) { std::cerr << "Cannot open " << filename << " for writing" << std::endl; exit(EXIT_FAILURE); } // create a temporary context to retrieve the image with HTTP GET struct soap *soap = soap_new(); soap->connect_timeout = soap->recv_timeout = soap->send_timeout = 10; // 10 sec // enable https connections with server certificate verification using cacerts.pem if (soap_ssl_client_context(soap, SOAP_SSL_SKIP_HOST_CHECK, NULL, NULL, "cacerts.pem", NULL, NULL)) report_error(soap); // HTTP GET and save image if (soap_GET(soap, endpoint, NULL) || soap_begin_recv(soap)) report_error(soap); std::cout << "Retrieving " << filename; if (soap->http_content) std::cout << " of type " << soap->http_content; std::cout << " from " << endpoint << std::endl; // this example stores the whole image in memory first, before saving it to the file // better is to copy the source code of soap_http_get_body here and // modify it to save data directly to the file. size_t imagelen; char *image = soap_http_get_body(soap, &imagelen); // NOTE: soap_http_get_body was renamed from soap_get_http_body in gSOAP 2.8.73 soap_end_recv(soap); fwrite(image, 1, imagelen, fd); fclose(fd); //cleanup soap_destroy(soap); soap_end(soap); soap_free(soap); } Since we are going to compile our application with WS-Discovery and the plugin, we must define WS-Discovery handlers even when we do not use them in our example application: void wsdd_event_Hello(struct soap *soap, unsigned int InstanceId, const char *SequenceId, unsigned int MessageNumber, const char *MessageID, const char *RelatesTo, const char *EndpointReference, const char *Types, const char *Scopes, const char *MatchBy, const char *XAddrs, unsigned int MetadataVersion) { } void wsdd_event_Bye(struct soap *soap, unsigned int InstanceId, const char *SequenceId, unsigned int MessageNumber, const char *MessageID, const char *RelatesTo, const char *EndpointReference, const char *Types, const char *Scopes, const char *MatchBy, const char *XAddrs, unsigned int *MetadataVersion) { } soap_wsdd_mode wsdd_event_Probe(struct soap *soap, const char *MessageID, const char *ReplyTo, const char *Types, const char *Scopes, const char *MatchBy, struct wsdd__ProbeMatchesType *ProbeMatches) { return SOAP_WSDD_ADHOC; } void wsdd_event_ProbeMatches(struct soap *soap, unsigned int InstanceId, const char *SequenceId, unsigned int MessageNumber, const char *MessageID, const char *RelatesTo, struct wsdd__ProbeMatchesType *ProbeMatches) { } soap_wsdd_mode wsdd_event_Resolve(struct soap *soap, const char *MessageID, const char *ReplyTo, const char *EndpointReference, struct wsdd__ResolveMatchType *match) { return SOAP_WSDD_ADHOC; } void wsdd_event_ResolveMatches(struct soap *soap, unsigned int InstanceId, const char * SequenceId, unsigned int MessageNumber, const char *MessageID, const char *RelatesTo, struct wsdd__ResolveMatchType *match) { } int SOAP_ENV__Fault(struct soap *soap, char *faultcode, char *faultstring, char *faultactor, struct SOAP_ENV__Detail *detail, struct SOAP_ENV__Code *SOAP_ENV__Code, struct SOAP_ENV__Reason *SOAP_ENV__Reason, char *SOAP_ENV__Node, char *SOAP_ENV__Role, struct SOAP_ENV__Detail *SOAP_ENV__Detail) { // populate the fault struct from the operation arguments to print it soap_fault(soap); // SOAP 1.1 soap->fault->faultcode = faultcode; soap->fault->faultstring = faultstring; soap->fault->faultactor = faultactor; soap->fault->detail = detail; // SOAP 1.2 soap->fault->SOAP_ENV__Code = SOAP_ENV__Code; soap->fault->SOAP_ENV__Reason = SOAP_ENV__Reason; soap->fault->SOAP_ENV__Node = SOAP_ENV__Node; soap->fault->SOAP_ENV__Role = SOAP_ENV__Role; soap->fault->SOAP_ENV__Detail = SOAP_ENV__Detail; // set error soap->error = SOAP_FAULT; // handle or display the fault here with soap_stream_fault(soap, std::cerr); // return HTTP 202 Accepted return soap_send_empty_response(soap, SOAP_OK); } We now compile the `ipcamera` application as follows: [command] c++ -o ipcamera -Wall -DWITH_OPENSSL -DWITH_DOM -DWITH_ZLIB \ -I. -I ~/gsoap-2.8/gsoap -I ~/gsoap-2.8/gsoap/plugin -I ~/gsoap-2.8/gsoap/custom \ main.cpp \ soapAdvancedSecurityServiceBindingProxy.cpp \ soapDeviceBindingProxy.cpp \ soapDeviceIOBindingProxy.cpp \ soapImagingBindingProxy.cpp \ soapMediaBindingProxy.cpp \ soapPTZBindingProxy.cpp \ soapPullPointSubscriptionBindingProxy.cpp \ soapRemoteDiscoveryBindingProxy.cpp \ ~/gsoap-2.8/gsoap/stdsoap2.cpp \ ~/gsoap-2.8/gsoap/dom.cpp \ ~/gsoap-2.8/gsoap/soapC.cpp \ ~/gsoap-2.8/gsoap/wsddClient.cpp \ ~/gsoap-2.8/gsoap/plugin/smdevp.c \ ~/gsoap-2.8/gsoap/plugin/mecevp.c \ ~/gsoap-2.8/gsoap/plugin/wsaapi.c \ ~/gsoap-2.8/gsoap/plugin/wsseapi.c \ ~/gsoap-2.8/gsoap/plugin/wsddapi.c \ -lcrypto -lssl -lz We presented an example C++ application to access an ONVIF service to retrieve a snapshot. We accomplished the following: - We build our application with several ONVIF WSDLs, some of them are not used in this simple example, to demonstrate to build steps; - We optimized the code size using wsdl2h options `-O4` and `-P`; - To build our application OpenSSL should be installed and the `libcrypto` and `libssl` libraries should be available. - We also build our application with zlib compression support using `-DWITH_ZLIB` and the `libz` library, which is optional. - WS-Security was used in this example for message timestamps and authentication credentials without message signatures to protect message integrity, which is not required since we transmit messages with https. - WS-Discovery was not used in this example. - We did not specify the smart way to handle *`xsd:duration`* in the `typemap.dat` file, because we don't access this type in this application. [![To top](../../images/go-up.png) To top](#) Security Matters ---------------- As we pointed out, transmitting ONVIF messages over plain http poses a risk. Instead, https should be used or the message headers and body should be signed with WS-Security to prevent tampering. Reliability and security of our software products is extremely important to us. The gSOAP toolkit is a commonly-used tookit to develop C/C++ Web services and REST Web APIs and is used by millions of online devices since the early 2000s. The latest versions of gSOAP are robust and secure and are regularly tested with analysis tools such as Fortify, Coverity and Valgrind. The toolkit is also frequently updated to met the latest OpenSSL API requirements. The software has one CVE dating back to June 2017 that was patched within 24 hours. The CVE affected ONVIF services that were not properly configured to prevent huge uploads exceeding 2GB of XML data. However, most ONVIF services developed with gSOAP at that time were not vulnerable at all because these were using the recommended [Apache module](../../doc/apache/html/index.html) and [ISAPI extension](../../doc/isapi/html/index.html) for gSOAP instead of being deployed as stand-alone gSOAP services. Please visit our [advisories](../../advisory.html) page to learn more about the history of the CVE. See [how to harden your application's robustness with timeouts and error handlers](../../tutorials.html#How_to_harden_your_application's_robustness_with_timeouts_and_error_handlers) for more details to secure your application. [![To top](../../images/go-up.png) To top](#)