Pelco Developer Network (PDN)

Supporting HTTP Digest Authentication in an ONVIF Client

HTTP Digest Authentication is the recommended authentication mechanism for web service applications that need to communicate with Sarix cameras running firmware version 1.9 or later. HTTP Digest does not send a password across the wire and may be used over both HTTPS and HTTP connections.

This article discusses the creation of an ONVIF C++ client application that uses HTTP Digest Authentication to communicate with a Sarix camera. ONVIF complements the Pelco API in Sarix cameras by providing additional functionality in the form of web service calls. The StartFirmwareUpgrade call in the Pelco API is an ONVIF web service that begins the process of upgrading the device firmware. StartFirmwareUpgrade requires a multi-step process, part of which is authentication.

Although the concepts of web services and SOAP-based communication are easy to grasp, the details can get messy. Many developers prefer to use classes that wrap SOAP calls instead of reading and writing raw XML strings. These classes hide the strings and the communication between client and server. There are several ways of creating wrappers around the SOAP calls necessary for a client to communicate with a camera or other ONVIF device. The sample provided in this article uses C++ proxy classes generated by the gSOAP toolkit, as discussed in Generating ONVIF Proxy Classes using gSOAP and Developing an ONVIF C++ Client Application. The sample in this article also includes Secure Sockets Layer support through OpenSSL, as described in Installing OpenSSL.

Poco objects comprise the HTTP request that sends the firmware file to the camera. Poco is a collection of open source C++ libraries. The classes are easy-to-use and lightweight.

The System > System Information menu item displays the current firmware version of the camera, as shown in Figure 1. Following a successful run of the application this value should reflect the updated version number.

Figure 1. Camera firmware version.
 

Complete Application Source Code

Here is the complete application source code in one listing. Snippets will be discussed in detail throughout the article.

// Standard library includes
#include <iostream>
#include <fstream>
#include <cstring>

// gSOAP includes
#include "soapDeviceBindingProxy.h"
#include "DeviceBinding.nsmap"
#include "httpda.h"

// Poco includes
#include "HTTPClientSession.h"
#include "HTTPRequest.h"
#include "HTTPResponse.h"
#include "StreamCopier.h"
#include "Exception.h"
#include "URI.h"
#include "File.h"
#include "HTMLForm.h"
#include "FilePartSource.h"


std::string host( "192.168.0.99" );
std::string firmwareFile( "/Downloads/D5118-1.9.2.19-20140318-1.9310-A1.10443.ppm" );

// Helper functions
std::string createOnvifEndpoint( std::string ipAddress );
std::string getKeyValue( std::string key, std::string delimiter, std::string buf );
bool containsString( std::string searchString, std::string buf );
std::vector tokenize( std::string delimiters, std::string buf );
bool canAuth( std::vector );

// Create a string representing the destination URI.
std::string createOnvifEndpoint( std::string ipAddress )
{
    std::string retval = "http://" + ipAddress + "/onvif/services";

    return retval;
}

// Does the buffer contain the string being searched for?
bool containsString( std::string searchString, std::string buf )
{
    bool retval = false;

    size_t index = buf.find( "WWW-Authenticate: Digest" );

    if ( index > 0 )
    {
        retval = true;
    }

    return retval;
}

// Given a name (key), find its value.
std::string getKeyValue( std::string key, std::string delimiter, std::string buf )
{
    std::string retval;
    
    size_t index = buf.find( key );

    if ( index > 0 )
    {
        size_t start = index + key.length();

        size_t stop = buf.find( delimiter, start );

        if ( stop > 0 )
        {
            retval = buf.substr( start, stop - start );
        }
    }

    return retval;
}

// Find multiple values in a key-value pair.
std::vector tokenize( std::string delimiters, std::string buf )
{
    std::vector retval;

    char *token = ::strtok( ( char * )buf.c_str(), delimiters.c_str() );

    while ( token != NULL )
    {
        retval.insert( retval.end(), std::string( token ) );

        token = strtok ( NULL, "," );
    }

    return retval;
}

// Find "auth" in a string. Very specific.
bool canAuth( std::vector v )
{
    bool retval = false;

    for ( unsigned int i = 0; i < v.size() && retval == false; i++ )
    {
        std::string s = v.at( i );

        if ( s.compare( "auth" ) == true )
        {
            retval = true;
        }
    }

    return retval;
}

int main(int argc, const char * argv[])
{
    struct soap soap;

    soap_init( &soap );

    DeviceBindingProxy proxy( soap );

    std::string endpoint = createOnvifEndpoint( host );

    soap_register_plugin( &soap, http_da );

    int result = SOAP_ERR;

    _tds__StartFirmwareUpgrade tds__StartFirmwareUpgrade;
    _tds__StartFirmwareUpgradeResponse tds__StartFirmwareUpgradeResponse;

    result = proxy.StartFirmwareUpgrade( endpoint.c_str(), NULL, \
        &tds__StartFirmwareUpgrade, &tds__StartFirmwareUpgradeResponse );

    if ( result == SOAP_OK )
    {
        std::cout << "Upload URI: " << tds__StartFirmwareUpgradeResponse.UploadUri << std::endl;
        std::cout << "Delay: " << tds__StartFirmwareUpgradeResponse.UploadDelay << std::endl;
        std::cout << "Expected down time: " << tds__StartFirmwareUpgradeResponse.ExpectedDownTime \
            << std::endl;
    }
    else if ( result == 401 )
    {
        std::cout << tds__StartFirmwareUpgradeResponse.soap->buf << std::endl;

        std::string buf( tds__StartFirmwareUpgradeResponse.soap->buf );

        if ( containsString( "WWW-Authenticate: Digest", buf ) )
        {
            std::string realm = getKeyValue( "realm=\"", "\"", buf );
            std::string nonce = getKeyValue( "nonce=\"", "\"", buf );
            std::string qop = getKeyValue( "qop=\"", "\"", buf );

            std::vector qopValues = tokenize( ",", qop );

            bool auth = canAuth( qopValues );

            std::cout << "realm:" << realm << " nonce:" << nonce << " qop:" << qop \
                << " auth: " << ( auth == 0 ? "No" : "Yes" ) << std::endl;

            proxy.userid = "admin";
            proxy.passwd = "admin";

            result = proxy.StartFirmwareUpgrade( endpoint.c_str(), NULL, 
                &tds__StartFirmwareUpgrade, &tds__StartFirmwareUpgradeResponse );

            if ( result == SOAP_OK )
            {
                std::cout << "Upload URI: " << tds__StartFirmwareUpgradeResponse.UploadUri << std::endl;
                std::cout << "Delay: " << tds__StartFirmwareUpgradeResponse.UploadDelay << std::endl;
                std::cout << "Expected down time: " << tds__StartFirmwareUpgradeResponse.ExpectedDownTime \
                    << std::endl;

                try
                {
                    sleep( tds__StartFirmwareUpgradeResponse.UploadDelay / 1000 );

                    Poco::URI uri( tds__StartFirmwareUpgradeResponse.UploadUri );

                    std::cout << "Path: " << uri.getPath() << std::endl;

                    Poco::Net::HTTPRequest request( Poco::Net::HTTPRequest::HTTP_POST, uri.getPath(), \
                        Poco::Net::HTTPMessage::HTTP_1_1 );

                    Poco::Net::HTMLForm form;

                    form.setEncoding( Poco::Net::HTMLForm::ENCODING_MULTIPART );

                    // The Part will be deallocated by the Form. See Poco documentation.
                    Poco::Net::FilePartSource *source = new Poco::Net::FilePartSource( firmwareFile );

                    form.addPart( "file", source );

                    form.prepareSubmit( request );

                    Poco::Net::HTTPClientSession session( host );

                    session.setTimeout( Poco::Timespan( 20, 0 ) );

                    form.write( session.sendRequest( request ) );

                    Poco::Net::HTTPResponse response;

                    std::istream &is = session.receiveResponse( response );

                    Poco::StreamCopier::copyStream( is, std::cout );
                }
                catch ( Poco::Exception &e )
                {
                    std::cout << e.displayText() << std::endl;
                }
            }

            soap_end( &soap );
        }
    }

    std::cout << std::endl;

    return 0;
}

 

Header Files

This application uses several C++ STL (Standard Template Library) classes, which are included at the top of the listing. The other #include files are gSOAP- and Poco-related.
 
// Standard library includes
#include <iostream>
#include <fstream>
#include <cstring>

// gSOAP includes
#include "soapDeviceBindingProxy.h"
#include "DeviceBinding.nsmap"
#include "httpda.h"

// Poco includes
#include "HTTPClientSession.h"
#include "HTTPRequest.h"
#include "HTTPResponse.h"
#include "StreamCopier.h"
#include "Exception.h"
#include "URI.h"
#include "File.h"
#include "HTMLForm.h"
#include "FilePartSource.h"
 
The soapDeviceBindingProxy.h and DeviceBinding.nsmap files are created by gSOAP during the class generation process. The .nsmap file maps individual namespaces to their definitions on the ONVIF site and other locations.
 
There is a chain of #include files starting at the top of soapDeviceBindingProxy.h. Those files are not listed explicitly in main.cpp but reside in the header paths specified in the project settings (shown later in this article), so the compiler can find them.
 
The file httpda.h is the gSOAP HTTP Digest Authentication plug-in header file; the project includes the corresponding httpda.c source code file. The plug-in gets loaded in the main function and handles authentication requests and responses on behalf of the client.
 
Poco classes provide the HTTP functionality needed to upload the firmware file to the camera. The header files can be found along the search paths specified in the project settings.
 
No manipulation of these header files is needed.
 

Helper Functions

 
The helper functions listed at the top perform common tasks and result in a simpler main function. In more complex applications several of the functions can be reused, while two (createOnvifEndpoint and canAuth) are ONVIF-specific.
  • createOnvifEndpoint creates a string representing the camera URI. This is where the camera is listening for incoming requests.
  • containsString determines if the buffer argument contains a particular string.
  • getKeyValue finds the value associated with the given key.
  • tokenize splits a string into its delimited values.
  • canAuth looks for the value "auth" in a string.

 

main() Function

The main() function is very simple but some of the data types may be unfamiliar. The soap structure is a gSOAP data structure that maintains state information, settings, function hooks, and much more. Look in stdsoap2.h for details.
    struct soap soap;

    soap_init( &soap );
 
After initializing the structure, allocate a DeviceBindingProxy object, create the endpoint string, and register the Digest Authentication plugin.
    DeviceBindingProxy proxy( soap );

    std::string endpoint = createOnvifEndpoint( host );

    soap_register_plugin( &soap, http_da );
 

Declare request and response objects (_tds__StartFirmwareUpgrade[Response]). Automatic variables work great here. The StartFirmwareUpgrade method call requires only the endpoint, and addresses of the request and response objects. The call is made synchronously. Behind the scenes the method maps to a web service call.

    int result = SOAP_ERR;

    _tds__StartFirmwareUpgrade tds__StartFirmwareUpgrade;
    _tds__StartFirmwareUpgradeResponse tds__StartFirmwareUpgradeResponse;

    result = proxy.StartFirmwareUpgrade( endpoint.c_str(), NULL, \
        &tds__StartFirmwareUpgrade, &tds__StartFirmwareUpgradeResponse );

 

StartFirmwareUpgrade Output

The first call to StartFirmwareUpgrade results in a 401 Error, as shown in the header:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Digest realm="Sarix", nonce="856df775e0d8b76725d9693a4794d032", qop="auth,auth-int"
Server: gSOAP/2.7
Content-Type: application/soap+xml; charset=utf-8
Content-Length: 717
Connection: close
Date: Fri, 16 Jan 1970 00:30:44 GMT
The SOAP payload has a few more details.
<?xml version="1.0" encoding="UTF-8"?>

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" xmlns:SOAP-ENC="http://www.w3.org/2003/05/soap-encoding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:wsa5="http://www.w3.org/2005/08/addressing" xmlns:ter="http://www.onvif.org/ver10/error">
  <SOAP-ENV:Body>
    <SOAP-ENV:Fault>
      <SOAP-ENV:Code>
        <SOAP-ENV:Value>SOAP-ENV:Sender</SOAP-ENV:Value>
        <SOAP-ENV:Subcode>
          <SOAP-ENV:Value>ter:NotAuthorized</SOAP-ENV:Value>
        </SOAP-ENV:Subcode>
      </SOAP-ENV:Code>
      <SOAP-ENV:Reason>
        <SOAP-ENV:Text xml:lang="en">Sender not authorized</SOAP-ENV:Text>
      </SOAP-ENV:Reason>
    </SOAP-ENV:Fault>
  </SOAP-ENV:Body>
  </SOAP-ENV:Envelope>FirmwareUpgrade>
    </tds:StartFirmwareUpgrade>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>
 

The code to handle the 401 error uses the helper functions to pull out several values: the realm, nonce, and qop. These values are retained in the response for communication back to the server, but are also shown here because they are required for Digest Authentication.

 
    else if ( result == 401 )
    {
        std::cout << tds__StartFirmwareUpgradeResponse.soap->buf << std::endl;

        std::string buf( tds__StartFirmwareUpgradeResponse.soap->buf );

        if ( containsString( "WWW-Authenticate: Digest", buf ) )
        {
            std::string realm = getKeyValue( "realm=\"", "\"", buf );
            std::string nonce = getKeyValue( "nonce=\"", "\"", buf );
            std::string qop = getKeyValue( "qop=\"", "\"", buf );

            std::vector qopValues = tokenize( ",", qop );

            bool auth = canAuth( qopValues );

            std::cout << "realm:" << realm << " nonce:" << nonce << " qop:" << qop \
                << " auth: " << ( auth == 0 ? "No" : "Yes" ) << std::endl;
 
The proxy class needs the username and password for the camera in order to make a subsequent call to StartFirmwareUpgrade.
            proxy.userid = "admin";
            proxy.passwd = "admin";

            result = proxy.StartFirmwareUpgrade( endpoint.c_str(), NULL, 
                &tds__StartFirmwareUpgrade, &tds__StartFirmwareUpgradeResponse );
The second call to StartFirmwareUpgrade, after constructing the Digest response, results in success:
<?xml version="1.0" encoding="UTF-8"?>

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" xmlns:SOAP-ENC="http://www.w3.org/2003/05/soap-encoding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:wsa5="http://www.w3.org/2005/08/addressing" xmlns:ter="http://www.onvif.org/ver10/error" xmlns:Device="http://www.onvif.org/ver10/device/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
  <SOAP-ENV:Body>
    <Device:StartFirmwareUpgradeResponse>
      <Device:UploadUri>http://192.168.0.99/filepost</Device:UploadUri>
      <Device:UploadDelay>PT30S</Device:UploadDelay>
      <Device:ExpectedDownTime>PT10M</Device:ExpectedDownTime>
    </Device:StartFirmwareUpgradeResponse>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

The values returned by the camera include: a downtime of 10 minutes once the process begins, a request to the client to wait 30 seconds before posting the firmware file, and the URI to which to post the firmware file. This sequence is described in the ONVIF Core Spec.

ExpectedDownTime = PT10M;
UploadDelay = PT30S;
UploadUri = "http://192.168.0.99/filepost";
 
The application then sleeps for the requested duration. Poco classes are then used to construct a HTTP POST request that contains an HTML form that contains the firmware file. After sending the form, the HTTP session waits for and prints the response from the server.
                try
                {
                    sleep( tds__StartFirmwareUpgradeResponse.UploadDelay / 1000 );

                    Poco::URI uri( tds__StartFirmwareUpgradeResponse.UploadUri );

                    std::cout << "Path: " << uri.getPath() << std::endl;

                    Poco::Net::HTTPRequest request( Poco::Net::HTTPRequest::HTTP_POST, uri.getPath(), \
                        Poco::Net::HTTPMessage::HTTP_1_1 );

                    Poco::Net::HTMLForm form;

                    form.setEncoding( Poco::Net::HTMLForm::ENCODING_MULTIPART );

                    // The Part will be deallocated by the Form. See Poco documentation.
                    Poco::Net::FilePartSource *source = new Poco::Net::FilePartSource( firmwareFile );

                    form.addPart( "file", source );

                    form.prepareSubmit( request );

                    Poco::Net::HTTPClientSession session( host );

                    session.setTimeout( Poco::Timespan( 20, 0 ) );

                    form.write( session.sendRequest( request ) );

                    Poco::Net::HTTPResponse response;

                    std::istream &is = session.receiveResponse( response );

                    Poco::StreamCopier::copyStream( is, std::cout );
                }
                catch ( Poco::Exception &e )
                {
                    std::cout << e.displayText() << std::endl;
                }
 
Uploading the file to the path portion of the URI results in a completion message from the camera.
Path: /filepost

The upload has been completed.
 
After the camera installs the firmware and reboots, the System Information screen reflects the updated Firmware Version. See Figure 2.
 
Figure 2. Upgraded firmware version.
 

Project Settings

Development environments differ in how you specify settings but these general concepts apply:
  • Add source code files and libraries
  • Set compiler flags, search paths, and language settings
  • Set linker flags

Examples of applying these steps are discussed in the next few sections.

Source code files

Figure 3 lists the source and library files included in this project. Most of the source files are gSOAP files and not all are C++. The gSOAP documentation, including the README.txt and INSTALL.txt files included in the archive, contain a lot of information regarding what needs to be included in a project.

Figure 3. Source file list.

The project also includes several libraries:

  • libgsoapssl++.a, the gSOAP C++ library with SSL support
  • libPocoFoundation.dylib, containing the core Poco classes
  • libPocoNet.dylib, the Poco networking classes

Compiler Flags

Figure 4 shows that the language and standard library dialect for C++ files is the GNU variant. This differs from the Xcode default of LLVM. Using GNU C++ fixes a compile-time error in the Pogo classes.

Figure 4. C++ language dialect.

 

The compiler needs to know where to find header files. In Figure 5 the User Header Search Paths setting includes the common /usr/local/include directory, and paths to the SSL, gSOAP, and Poco headers.

 

Figure 5. Library and header search paths.

 

OpenSSL support needs to be compiled in to the product. This step is outlined in the gSOAP Install.txt file, and illustrated in Figure 6 with the -D flag.

Figure 6. Compiler flags.

 

Linking

The linker needs to link against the SSL libraries crypto and ssl. The actual file names will vary by platform. For example, on Mac OS X and Darwin the full name is libcrypto.dylib, but the -l flag does not require the "lib" or ".dylib" portions of the file name, as shown in Figure 7.

Figure 7. Linker flags.

 

Using the techniques demonstrated in this article you can create a C++ ONVIF client application that uses HTTP Digest Authentication.
 

For More Information…

The links below provide additional information regarding the tools referenced in this article.