Main Page | Namespace List | Class Hierarchy | Alphabetical List | Class List | Directories | File List | Namespace Members | Class Members | File Members | Related Pages

Tutorial 4:SSL Client/Server database example

Prerequisite

Reading Tutorial 1: Simple Object Persistence will familiarize you with the data structures used below. Tutorial 3: TCP Client/Server Demonstration will explain how to use the TCP transport mechanism, which has usage to the SSL transport.

Overview

This tutorial demonstrates the creation of a simple database containing one record type - the ExtendedPersonRecord from Tutorial 1: Simple Object Persistence . The requirements for the client/server system are: The development consists of four steps:

Building the Database and Database Driver

Database

The first step in building a shared personal information database is to sketch out the series of operations that can be performed on the data. Remote clients should be able to query the database, insert, modify remove and search for records. To keep things simple, the use of std::multimap from Tutorial 1: Simple Object Persistence will be used. The database is implemented as a C++ class storing pointers to ExtendedPersonRecords and uses std::multimap containers for fast query operations. MynahSA's TCP and SSL servers are inherently threaded; therefore, class Database must use an some form of synchronization primitive. This tutorial uses MynahSA::Lock to prevent race conditions; however, any form of synchronization primitive could be used. The thread level design design can be visualized like this:

server_diag1.png

Server Diagram

To keep things simple, each database operation returns an instance of DBResult. Conceptually, class DBResult needs to return a set of hits - those records in the database matching the query request, a success or failure flag and an error string in the event that a failure has occurred. To simplify the process, three type definitions are made:

typedef SHARED_PTR<ExtendedPersonRecord> spRecord;
typedef DBHit spRecord;
typedef std::list<DBHit> DBHitList;

Class DBResult is derived from MynahSA::IoBase to allow results from database operations to be archived, and the class definition is:

class DBResult : public MynahSA::IoBase { 
public:
  enum Result { Undefined, Ok, Error };
  DBResult();
  DBResult(Result res, const std::string& what, const DBHitList& hits);
  DBResult(const DBResult& result);  
  ~DBResult();

  std::string ioName() const;
  void serialize(MynahSA::Archive& ar);

  const DBHitList& getHits() const;
  const std::string& what() const;
  bool isOk() const { return _result == Ok; }
  
private:
  Result _result;
  std::string _whatString;
  DBHitList _hits;
  
};

The use of class DBResult is fairly clear: When a result is received the isOk() method is used to determine if an error occurred. In the event that something went wrong the error message is displayed, otherwise, the list of successful hits are displayed.

The next step is to define class Database. Because Database is shared amongst all threads the a MynahSA::Mutex is stored in the private section:

#include <boost/thread.hpp>
#include <boost/utility.hpp>
#include <boost/thread/condition.hpp>
#include <boost/thread/thread.hpp>

class Database : public MynahSA::IoBase { 
public:
...
  DBResult queryName(const std::string& nameStart, 
                     const std::string& nameEnd) const;  
  DBResult queryAge(int ageStart, int ageEnd) const;
  DBResult queryEyeColor(PersonRecord::EyeColor ec) const;
  DBResult queryAll() const;
  DBResult modifyRecord(unsigned int uid, spRecord newRecord);
  DBResult deleteRecord(unsigned int uid);
  DBResult insertRecord(spRecord newRecord);
...
private:
  mutable MynahSA::Mutex _mutex;
};
The omitted detail in class Database can be seen in examples/tutorial4/database.hpp.

The implementation of class Database uses a simple macro LOCK which uses a scope sensitive lock (MynahSA::Lock) to making the user methods atomic:

#define LOCK Lock __l(_mutex)
The implementation of queryName is shown below:
DBResult Database::queryName(const string& nameStart, 
                             const string& nameEnd) const { 
  LOCK;

  DBHitList hl;

  string nameEndQuery;
  if (nameEnd == "") { 
    nameEndQuery = nameStart;
  } else {
    nameEndQuery = nameEnd;
  }
  
  basicQuery(nameStart, nameEndQuery, _nameMap, hl);
  
  return DBResult(DBResult::Ok, "", hl);
}

The implementation of each user method starts with the macro LOCK. The locking of each user method is essential: Race conditions could easily put the private data members of class Database out of sync and cause serious problems.

The above implementation takes two string parameters. The two strings are used to denote a range of names. In the event that the second string nameEnd is empty, the behavior of the queryName function is to return only those names matching nameStart. Finally, the implementation of queryName depends on template helper function basicQuery. basicQuery selects records within the range of its first two parameters from the multimap supplied in the third parameter and copies the matching hits onto the hitlist, the final parameter.

Database Driver

A database driver is just an abstract base class that implements an interface to class Database. For this example, the database driver implementation wraps each user method for class database, and provides hiding for the spRecord parameters. Class DatabaseDriver has the following method interface:
class DatabaseDriver { 
public:
  DatabaseDriver();
  virtual ~DatabaseDriver();
  virtual DBResult queryName(const std::string& nameStart, 
                             const std::string& nameEnd) const = 0;
  virtual DBResult queryAge(int ageStart, int ageEnd) const = 0;
  virtual DBResult queryEyeColor(PersonRecord::EyeColor ec) const = 0;
  virtual DBResult queryAll() const = 0;
  virtual DBResult modifyRecord(unsigned int uid, 
                                const std::string& name,
                                int age,
                                PersonRecord::EyeColor ec) = 0;
  virtual DBResult deleteRecord(unsigned int uid)=0;
  virtual DBResult insertRecord(const std::string& name,
                                int age,
                                PersonRecord::EyeColor ec) = 0;
};

Building the Parser and basic testing

The parser is used to take commands entered on the console and convert them into actions on a DatabaseDriver instance. The implementation is provided in examples/tutorial4/parser.cpp. The parser consists of two parts:

The Parser class performs one of two things when a string is fed to it:

The Parser class uses the following definition, taking a DatabaseDriver instance as a constructor parameter:

class Parser { 
public:
  Parser(DatabaseDriver& dbd);
  DBResult operator()(const std::string& s);

private:
  DatabaseDriver& _driver;
};

When operator() is invoked, the string presented is parsed and actions on the database driver are called.

To test the functionality of the database prior to implementing a client/server system a trivial wrapper class DatabaseFileDriver is derived from DatabaseDriver containing an instance of Database. The implementation is trivial and can be used to provide a direct console to database interface. The code shown below is from examples/tutorial4/directdbtest.cpp and was used to test the implementation of class Database prior to developing the client/server interface:

// a direct database test:
//   access and use a database from a file.
//   read commands from stdin, process them on the database.
//   report errors if they occur.
//   write the database to disk on exit

#include <iostream>
#include <string>
#include "databasefiledriver.hpp"
#include "parser.hpp"

using namespace std;

int main() {
  DatabaseFileDriver myDriver("testdb.dat");
  
  Parser myParser(myDriver);
  
  string cmd;
  // write the prompt
  cout << "> "; cout.flush();
  // while data is available, loop:
  while (getline(cin, cmd)) {
    try {
      if (!cmd.empty()) { 
        // parse command, write result
        DBResult res = myParser(cmd);
        cout << res << endl;
      }
    } catch (logic_error& le) { 
      cerr << le.what() << endl;
    }
    cout << "> "; cout.flush();
  }
}

The code above provides an overview of how the networked client will operate: A DatabaseDriver implementation is created and used by an instance of class Parser that handles command input. The mechanism by which the DatabaseDriver communicates with the underlying database is implementation specific, and lends itself nicely to hiding a network interface.

Network DatabaseDriver interface

The implementation of a networked database driver requires the transmission of requests from client to server, enabling the server to perform the required actions. The return path requires transmitting the object DBResult produced by the database instance to the client. Since DBResult already derives from MynahSA::IoBase the necessary work for the return path is complete.

The client to server path is, however, more complex and two alternative design routes can be taken:

Both approaches have their advantages and disadvantages: The first approach requires less work; only one class is required, whereas the second requires one class per user database method. The second approach is cleaner by design and does not require transmission of empty member variables, which wastes network bandwidth. For the sake of this tutorial, the first approach will be used and a class named DBRequest will carry all of the necessary data members from client to server.

The following diagram shows the protocol:

server_diag2.png

Server Protocol Diagram

The implementation of class DBRequest will use the following header:

class DBRequest : public MynahSA::IoBase {
public:
  enum RequestType { UNKNOWN,
                     QUERY_NAME, 
                     QUERY_AGE, 
                     QUERY_EYE_COLOR, 
                     QUERY_ALL, 
                     MODIFY_RECORD, 
                     DELETE_RECORD, 
                     INSERT_RECORD };
  RequestType _request;  
  std::string _name;
  std::string _name2;
  int _age;
  int _age2;
  unsigned int _uid;
  PersonRecord::EyeColor _eyeColor;

  DBRequest(RequestType request);
  std::string ioName() const;
  void serialize(MynahSA::Archive& ar);
};

This class is treated like a C struct, and the data members are left available for public access. The serialize method is responsible (like all other serialize methods) for placing each of the data members onto an Archive instance.

Using class DBRequest a database driver can be created:

class DatabaseSSLRPCDriver : public DatabaseDriver { 
public:
  DatabaseSSLRPCDriver(MynahSA::SSLRPCClient& client);
  ~DatabaseSSLRPCDriver();
  
  DBResult queryName(const std::string& nameStart, 
                     const std::string& nameEnd) const;  
  DBResult queryAge(int ageStart, int ageEnd) const;
  DBResult queryEyeColor(PersonRecord::EyeColor) const;
  DBResult queryAll() const;
  DBResult modifyRecord(unsigned int uid, 
                        const std::string& name,
                        int age,
                        PersonRecord::EyeColor ec);
  DBResult deleteRecord(unsigned int uid);
  DBResult insertRecord(const std::string& name,
                        int age,
                        PersonRecord::EyeColor ec);
private:
  // Perform RPC and convert result from an IoBase to a DBResult, 
  //   or throw a logic error
  DBResult rpcIt(const SHARED_PTR<MynahSA::IoBase>& req) const;
  
  mutable MynahSA::SSLRPCClient& _client;
};

The DatabaseSSLRPCDriver constructor takes a previously connected SSLRPCClient object and uses it to communicate with the server. The following code segment shows the implementation of the queryName and rpcIt method. All other database methods follow the same structure. Refer to examples/tutorial4/databasesslrpcdriver.cpp for further details.

DBResult DatabaseSSLRPCDriver::rpcIt(const SHARED_PTR<IoBase>& req) const { 
  SHARED_PTR<IoBase> ptr(_client.rpc(req));
  if (ptr->ioName() == "DBResult") { 
    return *static_pointer_cast<DBResult>(ptr);
  } else { 
    throw logic_error("DatabaseSSLRPCDriver::rpcIt - "
                      "failed to convert IoBase pointer to DBResult pointer");
  }
}

DBResult DatabaseSSLRPCDriver::queryName(const string& nameStart, 
                                         const string& nameEnd) const {
  SHARED_PTR<DBRequest> dbr(new DBRequest(DBRequest::QUERY_NAME));
  dbr->_name = nameStart;
  dbr->_name2 = nameEnd;
  return rpcIt(dbr);
}

Server side implementation

To complete the network interface the server must be able to receive DBRequest objects, perform the required actions and return a DBResult object. This behavior is implemented in class DBRequestResponseHandler. The use of RequestResponseHandler here is slightly different than in Tutorial 3 because the constructor function DBRequestResponseHandler::DBRequestResponseHandler takes a reference to a Database instance:

class DBRequestResponseHandler : public MynahSA::RequestResponseHandler { 
public:
  DBRequestResponseHandler(Database& db);
  ~DBRequestResponseHandler();
  SHARED_PTR<MynahSA::IoBase> 
    operator()(const SHARED_PTR<MynahSA::IoBase>& req);
private:
  Database& _db;
};

Each time an incoming connection is received by MynahSA::SSLServer a separate thread is spawned and given access to the RequestResponseHandler instance. Because each instance holds a reference to the database, each thread can perform the required database calls. The fundamental difference between a stateless server (as in Tutorial 3: TCP Client/Server Demonstration) and a server with state is in the state of the RequestResponseHandler implementation. The implementation of DBRequestResponseHandler is straight-forward: Incoming requests (as class MynahSA::IoBase) are converted into DBRequest classes, the actions are performed and the resulting DBResult is returned to from the server. The implementation from examples/tutorial4/dbrequestresponsehandler.cpp is shown below:

SHARED_PTR<MynahSA::IoBase> 
DBRequestResponseHandler::operator()(const SHARED_PTR<IoBase>& req) { 
  if (req->ioName() == DBRequest().ioName()) {
    SHARED_PTR<DBRequest> dbreq = static_pointer_cast<DBRequest>(req);

    DBResult result;
    switch (dbreq->_request) { 
    case DBRequest::QUERY_NAME:
      result = _db.queryName( dbreq->_name, dbreq->_name2 );
      break;
    case DBRequest::QUERY_AGE:
      result = _db.queryAge( dbreq->_age, dbreq->_age2 );
      break;
    case DBRequest::QUERY_EYE_COLOR:
      result = _db.queryEyeColor( dbreq->_eyeColor );
      break;
    case DBRequest::QUERY_ALL:
      result = _db.queryAll();
      break;
    case DBRequest::MODIFY_RECORD:
      result = _db.modifyRecord( 
                 dbreq->_uid, 
                 spRecord(new ExtendedPersonRecord(dbreq->_name, 
                                                   dbreq->_age, 
                                                   dbreq->_eyeColor) ) );
      break;
    case DBRequest::DELETE_RECORD:
      result = _db.deleteRecord( dbreq->_uid );
      break;
    case DBRequest::INSERT_RECORD:
      result = _db.insertRecord(
                 spRecord( new ExtendedPersonRecord(dbreq->_name, 
                                                    dbreq->_age, 
                                                    dbreq->_eyeColor) ) );
      break;
    default:
      result = DBResult(DBResult::Error, "Unknown request", DBHitList());
    };
    return SHARED_PTR<IoBase>(new DBResult(result));    
  } else { 
    throw logic_error("Error, invalid request object");
  }
}

Summary

This section presented three fundamental steps to building a networked client/server application: All client/server applications using MynahSA follow this design process.

Client and Server implementations

Client

The client implementation follows closely the test program described above, with the addition of the SSL connection setup. The constructor for SSLRPCClient is identical to the TCPRPCClient constructor, however, two additional parameters: certificateFile and pkeyFile may be provided. The additional parameters allow the client to present an SSL certificate file for authentication by the server. MynahSA's SSLServer and SSLRPCClient have two modes of communication: For unauthenticated communication the optional parameters certificateFile and pkeyFile may be omitted in SSLRPCClient construction. In this example, both methods are supported by providing optional command line arguments, "-cert <certificate file name>" and "-pkey <private key file". If these options are specified, the file names will be provided to the constructor. The entire main routine for the ssl client (from examples/tutorial4/ssldbclient.cpp) is:

int main(int argc, char** argv) {
  saInit(); // initialize WinSock, SSL, etc...
  try { 
    string certificateFile = "";
    string pkeyFile = "";
    string machineName ="";
    int port=0;
    
    // parse arguments
    parseClientArgs(argc, argv, machineName, port, certificateFile, pkeyFile);
  
    // create the constructor object.
    DBRegister myConstructor;
    
    cerr << "Performing setup" << endl;
    // construct ssl rpc client
    SSLRPCClient myClient(myConstructor, 
                          machineName, 
                          port, 
                          certificateFile, 
                          pkeyFile);
    DatabaseSSLRPCDriver myDriver(myClient);
    Parser myParser(myDriver);
    
    string cmd;
    cout << "< ";
    cout.flush();
    while (getline(cin, cmd)) {
      try {
        if (!cmd.empty()) { 
          DBResult res = myParser(cmd);
          cout << res << endl;
        }
      } catch (logic_error& le) { 
        cerr << le.what() << endl;
      }
      cout << "> ";
      cout.flush();
    }
  } catch(StringCastException& bc) { 
    cerr << "Port number could not be converted into an integer."
            "  Check arguments!" << endl;
    return 1;
  } catch (ExceptionBase& eb) { 
    cerr << "Caught a fatal exception: " << eb.what() << endl;
    return 1;
  } catch (...) { 
    cerr << "Uncaught exception.  Check and debug!" << endl;
    return 1;
  }
  return 0;
}

The logic behind the client is simple: Create a constructor registration object (DBRegister), build the SSLRPCClient object, bind a DatabaseSSLRPCDriver to it. Finally, build a parser instance on top of the driver and process commands.

What is different from this implementation and previous examples is the exception handling. In this example, std::logic_error is trapped in the inner loop; this exception is thrown when parse errors are encountered and are considered non-fatal exceptions. There are two potential fatal exceptions which the program may generate: MynahSA::StringCastException may be generated by the command line parser in attempting to convert a string to an integer for the port number, and MynahSA::ExceptionBase exceptions may be generated during the client setup phase and/or during the communications. A MynahSA::ExceptionBase will be generated if the server vanishes while a connection is still established.

Server

The server setup is similar to the TCPServer setup that was used in Tutorial 3 - however, using the SSL server requires a few additional steps. The following code sequences is from examples/tutorial4/ssldbserver.cpp. The first step in setting up the server is to create the database instance and the request response handler instance:

  // step 0: Initialize the database
  Database db("testdb.dat");

  // step 1: build the ping request response handler
  DBRequestResponseHandler drh(db);
    
  // step 2: create the stream registration object
  DBRegister myConstructor;

The code path is then split depending on whether or not the user supplied a certificate and private key pair or not. In the event that certificate and private key files were provided the program starts an authenticating server using the following code path:

  SSLConnectionCertVerifier myVerifier;
    
  // step 4: instantiate a connection manager on the handler and the
  //         stream constructor
  SSLConnectionManager cm(drh,
                          myConstructor,
                          myVerifier); 
                                                
  // step 5: build an ssl server on top of the connection manager instance
  SSLRPCServer server(cm, certFile, privKeyFile, serverPort,caFile);

The SSLConnectionCertVerifier object is a functor provided to the server that is used to validate or drop connections. The supplied implementation checks that the client's certificate has a canonical name (CN) field and that it matches the network name of the connecting machine. This is intended to prevent a stolen certificate from being used on a different machine.

In the event that no certificate file was provided a default certificate is created using the alternative code path, shown below:

      SSLConnectionVerifier myVerifier;
      
      // step 4: instantiate a connection manager on the handler and the 
      //stream constructor
      SSLConnectionManager cm(drh,
                              myConstructor,
                              myVerifier); 
                                                
      // step 5: build an ssl server on top of the connection 
      //manager instance
      mkcert(&cert, 
             &pkey, 
             1024, 
             0, 
             760);  // create cert and pkey - completely self signed 1024 bit
      SSLRPCServer server(cm, cert, pkey, serverPort,caFile);

The SSLConnectionVerifier class is a connection verifier that accepts every connection coming in without examining the client's DNS information.

Note: even though SSLConnectionVerifier does no specific information checking, OpenSSL will not accept an incoming connection if the certificate does not match the certificate authority file - this means that clients placed behind NATted networks that have no DNS name can connect, provided they have a signed certificate.

The call to MynahSA::mkcert is used to create a self signed certificate and associated private key. The 1024 specifies that a 1024 bit RSA cipher will be used, the serial number will be zero and the certificate will be valid for 760 days.

Whether using authentication or not both code paths share the same code for handling incoming connections:

      while (running) { 
        try { 
          server.checkClients(5);
        } catch (const ServerConnectionException& sce) { 
          cerr << "Connection error detected: " << sce.what() << endl;
        }
      }

The boolean variable running is used to control termination of the server. A signal handler is installed to manage keyboard interrupts and set running to false. The exception ServerConnectionException is handled locally as it pertains to exceptions raised during the establishment of a TCP connection and occur within the main thread. Once a connection is established, a separate thread with local exception handling is used to manage all network input and output for that thread.

One final part of the server code is the trapping of ServerException exceptions. These exceptions are viewed as fatal and only occur when the master TCP port (to allow incoming connections) is unavailable or there is a serious error with OpenSSL startup. For further details, see examples/tutorial4/ssldbserver.cpp.

Client authentication with SSL Certificates

The final part in this tutorial covers the steps required to build the certificate files for both server and client and build a certificate authority file to sign certificates for client access.

The procedure shown below is an interactive session using OpenSSL's tool chain to build the certificate files. The session was conducted on a fictional domain "brettnet.net" with a machine named "warchief." Because the authentication process checks for validity of domain names it is necessary to provide the correct domain name of both the client and server machines in this process.

The first step is to build the root private key and root certificate. The root certificate will serve as the basis for signing all other certificates, thus restricting the clients that can access the server.

[bms20@warchief tmp]$ openssl req -x509 -nodes -days 365 -newkey rsa:1024 \
                                  -keyout rootkey.pem -out rootcert.pem
Generating a 1024 bit RSA private key
.................................++++++
......................++++++
writing new private key to 'rootkey.pem'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [GB]:
State or Province Name (full name) [Berkshire]:Cambridgeshire
Locality Name (eg, city) [Newbury]:Cambridge
Organization Name (eg, company) [My Company Ltd]:Mynah-Software LTD
Organizational Unit Name (eg, section) []:Development
Common Name (eg, your name or your server's hostname) []:warchief.brettnet.net
Email Address []:brett@mynah-software.com
[bms20@warchief tmp]$
The root certificate must be converted into a Certificate Authority (CA) file format prior to use by the server:

[bms20@warchief tmp]$ openssl x509 -in rootcert.pem -text > CAfile.pem

Next, a server certificate request and private key are generated for the server certificate. The entry of the details for the server have been omitted, however, like the creation of the root certificate above, the correct DNS name for the server is required:

[bms20@warchief tmp]$ openssl req -nodes -newkey rsa:1024 \
                              -keyout serverkey.pem -new  \
                              -days 365 -out serverreq.pem

The server certificate is produced by filling in the certificate request - this certificate is signed by the certificate authority:

[bms20@warchief tmp]$ openssl x509 -days 180 -CA rootcert.pem \
                                   -CAkey rootkey.pem -req -CAcreateserial \
                                   -CAserial serverCA.srl -in serverreq.pem \
                                   -out servercert.pem
The files serverkey.pem and servercert.pem and (optionally) CAfile.pem may now be used as the certificate, private key and certificate authority for the database server program. If the optional CA file is provided, only clients possessing a certificate signed by the certificate authority will be allowed access to the server. To build an authorized client certificate, the client must do the following steps:

A helper script, examples/tutorial4/make_certificates.sh is provided to help with the process of creating the necessary certificates.

Finally, to start the server in authenticated mode, issue the command:

./ssldbserver -cert servercert.pem -pkey serverkey.pem -CAfile CAfile.pem

And a sample session using the client could be:

./ssldbclient warchief 7501 -cert clientcert.pem -pkey clientkey.pem
Performing setup
> queryAll()
Hit unique id: 1
Name: Brett
Age:  33
Eye Color: Blue
Friends:

Hit unique id: 2
Name: Joe
Age:  29
Eye Color: Brown
Friends:

Hit unique id: 3
Name: Jim
Age:  60
Eye Color: Blue
Friends:


>

Conclusions

This tutorial has shown several things: Hopefully, this tutorial will help you get started with your own secure server projects!