Commit fe5e7003 authored by Michal 'vorner' Vaner's avatar Michal 'vorner' Vaner

Merge branch 'work/sign2'

parents 67727b62 5178a891
......@@ -20,6 +20,7 @@
#include <cassert>
#include <iostream>
#include <vector>
#include <memory>
#include <boost/bind.hpp>
......@@ -43,6 +44,7 @@
#include <dns/rrset.h>
#include <dns/rrttl.h>
#include <dns/message.h>
#include <dns/tsig.h>
#include <datasrc/query.h>
#include <datasrc/data_source.h>
......@@ -73,6 +75,7 @@ using namespace isc::xfr;
using namespace isc::asiolink;
using namespace isc::asiodns;
using namespace isc::server_common::portconfig;
using boost::shared_ptr;
class AuthSrvImpl {
private:
......@@ -85,11 +88,14 @@ public:
isc::data::ConstElementPtr setDbFile(isc::data::ConstElementPtr config);
bool processNormalQuery(const IOMessage& io_message, MessagePtr message,
OutputBufferPtr buffer);
OutputBufferPtr buffer,
auto_ptr<TSIGContext> tsig_context);
bool processAxfrQuery(const IOMessage& io_message, MessagePtr message,
OutputBufferPtr buffer);
OutputBufferPtr buffer,
auto_ptr<TSIGContext> tsig_context);
bool processNotify(const IOMessage& io_message, MessagePtr message,
OutputBufferPtr buffer);
OutputBufferPtr buffer,
auto_ptr<TSIGContext> tsig_context);
IOService io_service_;
......@@ -116,6 +122,9 @@ public:
/// Addresses we listen on
AddressList listen_addresses_;
/// The TSIG keyring
const shared_ptr<TSIGKeyRing>* keyring_;
private:
std::string db_file_;
......@@ -139,6 +148,7 @@ AuthSrvImpl::AuthSrvImpl(const bool use_cache,
memory_datasrc_class_(RRClass::IN()),
statistics_timer_(io_service_),
counters_(verbose_mode_),
keyring_(NULL),
xfrout_connected_(false),
xfrout_client_(xfrout_client)
{
......@@ -241,7 +251,9 @@ public:
void
makeErrorMessage(MessagePtr message, OutputBufferPtr buffer,
const Rcode& rcode, const bool verbose_mode)
const Rcode& rcode, const bool verbose_mode,
std::auto_ptr<TSIGContext> tsig_context =
std::auto_ptr<TSIGContext>())
{
// extract the parameters that should be kept.
// XXX: with the current implementation, it's not easy to set EDNS0
......@@ -272,7 +284,11 @@ makeErrorMessage(MessagePtr message, OutputBufferPtr buffer,
message->setRcode(rcode);
MessageRenderer renderer(*buffer);
message->toWire(renderer);
if (tsig_context.get() != NULL) {
message->toWire(renderer, *tsig_context);
} else {
message->toWire(renderer);
}
if (verbose_mode) {
cerr << "[b10-auth] sending an error response (" <<
......@@ -446,29 +462,51 @@ AuthSrv::processMessage(const IOMessage& io_message, MessagePtr message,
}
// Perform further protocol-level validation.
// TSIG first
// If this is set to something, we know we need to answer with TSIG as well
std::auto_ptr<TSIGContext> tsig_context;
const TSIGRecord* tsig_record(message->getTSIGRecord());
TSIGError tsig_error(TSIGError::NOERROR());
// Do we do TSIG?
// The keyring can be null if we're in test
if (impl_->keyring_ != NULL && tsig_record != NULL) {
tsig_context.reset(new TSIGContext(tsig_record->getName(),
tsig_record->getRdata().
getAlgorithm(),
**impl_->keyring_));
tsig_error = tsig_context->verify(tsig_record, io_message.getData(),
io_message.getDataSize());
}
bool sendAnswer = true;
if (message->getOpcode() == Opcode::NOTIFY()) {
sendAnswer = impl_->processNotify(io_message, message, buffer);
if (tsig_error != TSIGError::NOERROR()) {
makeErrorMessage(message, buffer, tsig_error.toRcode(),
impl_->verbose_mode_, tsig_context);
} else if (message->getOpcode() == Opcode::NOTIFY()) {
sendAnswer = impl_->processNotify(io_message, message, buffer,
tsig_context);
} else if (message->getOpcode() != Opcode::QUERY()) {
if (impl_->verbose_mode_) {
cerr << "[b10-auth] unsupported opcode" << endl;
}
makeErrorMessage(message, buffer, Rcode::NOTIMP(),
impl_->verbose_mode_);
impl_->verbose_mode_, tsig_context);
} else if (message->getRRCount(Message::SECTION_QUESTION) != 1) {
makeErrorMessage(message, buffer, Rcode::FORMERR(),
impl_->verbose_mode_);
impl_->verbose_mode_, tsig_context);
} else {
ConstQuestionPtr question = *message->beginQuestion();
const RRType &qtype = question->getType();
if (qtype == RRType::AXFR()) {
sendAnswer = impl_->processAxfrQuery(io_message, message, buffer);
sendAnswer = impl_->processAxfrQuery(io_message, message, buffer,
tsig_context);
} else if (qtype == RRType::IXFR()) {
makeErrorMessage(message, buffer, Rcode::NOTIMP(),
impl_->verbose_mode_);
impl_->verbose_mode_, tsig_context);
} else {
sendAnswer = impl_->processNormalQuery(io_message, message, buffer);
sendAnswer = impl_->processNormalQuery(io_message, message, buffer,
tsig_context);
}
}
......@@ -477,7 +515,8 @@ AuthSrv::processMessage(const IOMessage& io_message, MessagePtr message,
bool
AuthSrvImpl::processNormalQuery(const IOMessage& io_message, MessagePtr message,
OutputBufferPtr buffer)
OutputBufferPtr buffer,
auto_ptr<TSIGContext> tsig_context)
{
ConstEDNSPtr remote_edns = message->getEDNS();
const bool dnssec_ok = remote_edns && remote_edns->getDNSSECAwareness();
......@@ -523,7 +562,11 @@ AuthSrvImpl::processNormalQuery(const IOMessage& io_message, MessagePtr message,
const bool udp_buffer =
(io_message.getSocket().getProtocol() == IPPROTO_UDP);
renderer.setLengthLimit(udp_buffer ? remote_bufsize : 65535);
message->toWire(renderer);
if (tsig_context.get() != NULL) {
message->toWire(renderer, *tsig_context);
} else {
message->toWire(renderer);
}
if (verbose_mode_) {
cerr << "[b10-auth] sending a response ("
......@@ -536,7 +579,8 @@ AuthSrvImpl::processNormalQuery(const IOMessage& io_message, MessagePtr message,
bool
AuthSrvImpl::processAxfrQuery(const IOMessage& io_message, MessagePtr message,
OutputBufferPtr buffer)
OutputBufferPtr buffer,
auto_ptr<TSIGContext> tsig_context)
{
// Increment query counter.
incCounter(io_message.getSocket().getProtocol());
......@@ -545,7 +589,8 @@ AuthSrvImpl::processAxfrQuery(const IOMessage& io_message, MessagePtr message,
if (verbose_mode_) {
cerr << "[b10-auth] AXFR query over UDP isn't allowed" << endl;
}
makeErrorMessage(message, buffer, Rcode::FORMERR(), verbose_mode_);
makeErrorMessage(message, buffer, Rcode::FORMERR(), verbose_mode_,
tsig_context);
return (true);
}
......@@ -572,7 +617,8 @@ AuthSrvImpl::processAxfrQuery(const IOMessage& io_message, MessagePtr message,
cerr << "[b10-auth] Error in handling XFR request: " << err.what()
<< endl;
}
makeErrorMessage(message, buffer, Rcode::SERVFAIL(), verbose_mode_);
makeErrorMessage(message, buffer, Rcode::SERVFAIL(), verbose_mode_,
tsig_context);
return (true);
}
......@@ -581,7 +627,8 @@ AuthSrvImpl::processAxfrQuery(const IOMessage& io_message, MessagePtr message,
bool
AuthSrvImpl::processNotify(const IOMessage& io_message, MessagePtr message,
OutputBufferPtr buffer)
OutputBufferPtr buffer,
std::auto_ptr<TSIGContext> tsig_context)
{
// The incoming notify must contain exactly one question for SOA of the
// zone name.
......@@ -590,7 +637,8 @@ AuthSrvImpl::processNotify(const IOMessage& io_message, MessagePtr message,
cerr << "[b10-auth] invalid number of questions in notify: "
<< message->getRRCount(Message::SECTION_QUESTION) << endl;
}
makeErrorMessage(message, buffer, Rcode::FORMERR(), verbose_mode_);
makeErrorMessage(message, buffer, Rcode::FORMERR(), verbose_mode_,
tsig_context);
return (true);
}
ConstQuestionPtr question = *message->beginQuestion();
......@@ -599,7 +647,8 @@ AuthSrvImpl::processNotify(const IOMessage& io_message, MessagePtr message,
cerr << "[b10-auth] invalid question RR type in notify: "
<< question->getType() << endl;
}
makeErrorMessage(message, buffer, Rcode::FORMERR(), verbose_mode_);
makeErrorMessage(message, buffer, Rcode::FORMERR(), verbose_mode_,
tsig_context);
return (true);
}
......@@ -662,7 +711,11 @@ AuthSrvImpl::processNotify(const IOMessage& io_message, MessagePtr message,
message->setRcode(Rcode::NOERROR());
MessageRenderer renderer(*buffer);
message->toWire(renderer);
if (tsig_context.get() != NULL) {
message->toWire(renderer, *tsig_context);
} else {
message->toWire(renderer);
}
return (true);
}
......@@ -772,3 +825,8 @@ void
AuthSrv::setDNSService(isc::asiodns::DNSService& dnss) {
dnss_ = &dnss;
}
void
AuthSrv::setTSIGKeyRing(const shared_ptr<TSIGKeyRing>* keyring) {
impl_->keyring_ = keyring;
}
......@@ -44,6 +44,9 @@ class MemoryDataSrc;
namespace xfr {
class AbstractXfroutClient;
}
namespace dns {
class TSIGKeyRing;
}
}
......@@ -374,6 +377,14 @@ public:
/// \brief Assign an ASIO DNS Service queue to this Auth object
void setDNSService(isc::asiodns::DNSService& dnss);
/// \brief Sets the keyring used for verifying and signing
///
/// The parameter is pointer to shared pointer, because the automatic
/// reloading routines of tsig keys replace the actual keyring object.
/// It is expected the pointer will point to some statically-allocated
/// object, it doesn't take ownership of it.
void setTSIGKeyRing(const boost::shared_ptr<isc::dns::TSIGKeyRing>*
keyring);
private:
AuthSrvImpl* impl_;
......
......@@ -47,6 +47,7 @@
#include <asiodns/asiodns.h>
#include <asiolink/asiolink.h>
#include <log/dummylog.h>
#include <server_common/keyring.h>
using namespace std;
using namespace isc::data;
......@@ -152,9 +153,14 @@ main(int argc, char* argv[]) {
cc_session = new Session(io_service.get_io_service());
cout << "[b10-auth] Configuration session channel created." << endl;
// We delay starting listening to new commands/config just before we
// go into the main loop to avoid confusion due to mixture of
// synchronous and asynchronous operations (this would happen in
// initializing TSIG keys below). Until then all operations on the
// CC session will take place synchronously.
config_session = new ModuleCCSession(specfile, *cc_session,
my_config_handler,
my_command_handler);
my_command_handler, false);
cout << "[b10-auth] Configuration channel established." << endl;
xfrin_session = new Session(io_service.get_io_service());
......@@ -190,6 +196,14 @@ main(int argc, char* argv[]) {
changeUser(uid);
}
cout << "[b10-auth] Loading TSIG keys" << endl;
isc::server_common::initKeyring(*config_session);
auth_server->setTSIGKeyRing(&isc::server_common::keyring);
// Now start asynchronous read.
config_session->start();
cout << "[b10-auth] Configuration channel started." << endl;
cout << "[b10-auth] Server started." << endl;
io_service.run();
......
......@@ -16,6 +16,8 @@
#include <vector>
#include <boost/shared_ptr.hpp>
#include <gtest/gtest.h>
#include <dns/message.h>
......@@ -25,8 +27,10 @@
#include <dns/rrtype.h>
#include <dns/rrttl.h>
#include <dns/rdataclass.h>
#include <dns/tsig.h>
#include <server_common/portconfig.h>
#include <server_common/keyring.h>
#include <datasrc/memory_datasrc.h>
#include <auth/auth_srv.h>
......@@ -50,6 +54,7 @@ using namespace isc::asiolink;
using namespace isc::testutils;
using namespace isc::server_common::portconfig;
using isc::UnitTestUtil;
using boost::shared_ptr;
namespace {
const char* const CONFIG_TESTDB =
......@@ -242,6 +247,139 @@ TEST_F(AuthSrvTest, AXFRSuccess) {
EXPECT_TRUE(xfrout.isConnected());
}
// Try giving the server a TSIG signed request and see it can anwer signed as
// well
TEST_F(AuthSrvTest, TSIGSigned) {
// Prepare key, the client message, etc
const TSIGKey key("key:c2VjcmV0Cg==:hmac-sha1");
TSIGContext context(key);
UnitTestUtil::createRequestMessage(request_message, opcode, default_qid,
Name("version.bind"), RRClass::CH(), RRType::TXT());
createRequestPacket(request_message, IPPROTO_UDP, &context);
// Run the message through the server
shared_ptr<TSIGKeyRing> keyring(new TSIGKeyRing);
keyring->add(key);
server.setTSIGKeyRing(&keyring);
server.processMessage(*io_message, parse_message, response_obuffer,
&dnsserv);
// What did we get?
EXPECT_TRUE(dnsserv.hasAnswer());
headerCheck(*parse_message, default_qid, Rcode::NOERROR(),
opcode.getCode(), QR_FLAG | AA_FLAG, 1, 1, 1, 0);
// We need to parse the message ourself, or getTSIGRecord won't work
InputBuffer ib(response_obuffer->getData(), response_obuffer->getLength());
Message m(Message::PARSE);
m.fromWire(ib);
const TSIGRecord* tsig = m.getTSIGRecord();
ASSERT_TRUE(tsig != NULL) << "Missing TSIG signature";
TSIGError error(context.verify(tsig, response_obuffer->getData(),
response_obuffer->getLength()));
EXPECT_EQ(TSIGError::NOERROR(), error) <<
"The server signed the response, but it doesn't seem to be valid";
}
// Give the server a signed request, but don't give it the key. It will
// not be able to verify it, returning BADKEY
TEST_F(AuthSrvTest, TSIGSignedBadKey) {
TSIGKey key("key:c2VjcmV0Cg==:hmac-sha1");
TSIGContext context(key);
UnitTestUtil::createRequestMessage(request_message, opcode, default_qid,
Name("version.bind"), RRClass::CH(), RRType::TXT());
createRequestPacket(request_message, IPPROTO_UDP, &context);
// Process the message, but use a different key there
shared_ptr<TSIGKeyRing> keyring(new TSIGKeyRing);
server.setTSIGKeyRing(&keyring);
server.processMessage(*io_message, parse_message, response_obuffer,
&dnsserv);
EXPECT_TRUE(dnsserv.hasAnswer());
headerCheck(*parse_message, default_qid, TSIGError::BAD_KEY().toRcode(),
opcode.getCode(), QR_FLAG, 1, 0, 0, 0);
// We need to parse the message ourself, or getTSIGRecord won't work
InputBuffer ib(response_obuffer->getData(), response_obuffer->getLength());
Message m(Message::PARSE);
m.fromWire(ib);
const TSIGRecord* tsig = m.getTSIGRecord();
ASSERT_TRUE(tsig != NULL) <<
"Missing TSIG signature (we should have one even at error)";
EXPECT_EQ(TSIGError::BAD_KEY_CODE, tsig->getRdata().getError());
EXPECT_EQ(0, tsig->getRdata().getMACSize()) <<
"It should be unsigned with this error";
}
// Give the server a signed request, but signed by a different key
// (with the same name). It should return BADSIG
TEST_F(AuthSrvTest, TSIGBadSig) {
TSIGKey key("key:c2VjcmV0Cg==:hmac-sha1");
TSIGContext context(key);
UnitTestUtil::createRequestMessage(request_message, opcode, default_qid,
Name("version.bind"), RRClass::CH(), RRType::TXT());
createRequestPacket(request_message, IPPROTO_UDP, &context);
// Process the message, but use a different key there
shared_ptr<TSIGKeyRing> keyring(new TSIGKeyRing);
keyring->add(TSIGKey("key:QkFECg==:hmac-sha1"));
server.setTSIGKeyRing(&keyring);
server.processMessage(*io_message, parse_message, response_obuffer,
&dnsserv);
EXPECT_TRUE(dnsserv.hasAnswer());
headerCheck(*parse_message, default_qid, TSIGError::BAD_SIG().toRcode(),
opcode.getCode(), QR_FLAG, 1, 0, 0, 0);
// We need to parse the message ourself, or getTSIGRecord won't work
InputBuffer ib(response_obuffer->getData(), response_obuffer->getLength());
Message m(Message::PARSE);
m.fromWire(ib);
const TSIGRecord* tsig = m.getTSIGRecord();
ASSERT_TRUE(tsig != NULL) <<
"Missing TSIG signature (we should have one even at error)";
EXPECT_EQ(TSIGError::BAD_SIG_CODE, tsig->getRdata().getError());
EXPECT_EQ(0, tsig->getRdata().getMACSize()) <<
"It should be unsigned with this error";
}
// Give the server a signed unsupported request with a bad signature.
// This checks the server first verifies the signature before anything
// else.
TEST_F(AuthSrvTest, TSIGCheckFirst) {
TSIGKey key("key:c2VjcmV0Cg==:hmac-sha1");
TSIGContext context(key);
// Pass a wrong opcode there. The server shouldn't know what to do
// about it.
UnitTestUtil::createRequestMessage(request_message, Opcode::RESERVED14(),
default_qid, Name("version.bind"),
RRClass::CH(), RRType::TXT());
createRequestPacket(request_message, IPPROTO_UDP, &context);
// Process the message, but use a different key there
shared_ptr<TSIGKeyRing> keyring(new TSIGKeyRing);
keyring->add(TSIGKey("key:QkFECg==:hmac-sha1"));
server.setTSIGKeyRing(&keyring);
server.processMessage(*io_message, parse_message, response_obuffer,
&dnsserv);
EXPECT_TRUE(dnsserv.hasAnswer());
headerCheck(*parse_message, default_qid, TSIGError::BAD_SIG().toRcode(),
Opcode::RESERVED14().getCode(), QR_FLAG, 0, 0, 0, 0);
// We need to parse the message ourself, or getTSIGRecord won't work
InputBuffer ib(response_obuffer->getData(), response_obuffer->getLength());
Message m(Message::PARSE);
m.fromWire(ib);
const TSIGRecord* tsig = m.getTSIGRecord();
ASSERT_TRUE(tsig != NULL) <<
"Missing TSIG signature (we should have one even at error)";
EXPECT_EQ(TSIGError::BAD_SIG_CODE, tsig->getRdata().getError());
EXPECT_EQ(0, tsig->getRdata().getMACSize()) <<
"It should be unsigned with this error";
}
TEST_F(AuthSrvTest, AXFRConnectFail) {
EXPECT_FALSE(xfrout.isConnected()); // check prerequisite
xfrout.disableConnect();
......
......@@ -192,8 +192,10 @@ ModuleCCSession::ModuleCCSession(
isc::data::ConstElementPtr(*config_handler)(
isc::data::ConstElementPtr new_config),
isc::data::ConstElementPtr(*command_handler)(
const std::string& command, isc::data::ConstElementPtr args)
const std::string& command, isc::data::ConstElementPtr args),
bool start_immediately
) :
started_(false),
session_(session)
{
module_specification_ = readModuleSpecification(spec_file_name);
......@@ -237,8 +239,21 @@ ModuleCCSession::ModuleCCSession(
}
}
if (start_immediately) {
start();
}
}
void
ModuleCCSession::start() {
if (started_) {
isc_throw(CCSessionError, "Module CC session already started");
}
// register callback for asynchronous read
session_.startRead(boost::bind(&ModuleCCSession::startCheck, this));
started_ = true;
}
/// Validates the new config values, if they are correct,
......@@ -355,45 +370,56 @@ ModuleCCSession::checkCommand() {
return (0);
}
std::string
ModuleCCSession::addRemoteConfig(const std::string& spec_name,
void (*handler)(const std::string& module,
ConstElementPtr),
bool spec_is_filename)
{
std::string module_name;
ModuleSpec rmod_spec;
if (spec_is_filename) {
// It's a file name, so load it
rmod_spec = readModuleSpecification(spec_name);
module_name =
rmod_spec.getFullSpec()->get("module_name")->stringValue();
ModuleSpec
ModuleCCSession::fetchRemoteSpec(const std::string& module, bool is_filename) {
if (is_filename) {
// It is a filename, simply load it.
return (readModuleSpecification(module));
} else {
// It's module name, request it from config manager
ConstElementPtr cmd = Element::fromJSON("{ \"command\": ["
"\"get_module_spec\","
"{\"module_name\": \"" +
module_name + "\"} ] }");
unsigned int seq = session_.group_sendmsg(cmd, "ConfigManager");
// Send the command
ConstElementPtr cmd(createCommand("get_module_spec",
Element::fromJSON("{\"module_name\": \"" + module +
"\"}")));
const unsigned int seq = session_.group_sendmsg(cmd, "ConfigManager");
// Wait for the answer
ConstElementPtr env, answer;
session_.group_recvmsg(env, answer, false, seq);
int rcode;
ConstElementPtr spec_data = parseAnswer(rcode, answer);
if (rcode == 0 && spec_data) {
rmod_spec = ModuleSpec(spec_data);
module_name = spec_name;
if (module_name != rmod_spec.getModuleName()) {
// received OK, construct the spec out of it
ModuleSpec spec = ModuleSpec(spec_data);
if (module != spec.getModuleName()) {
// It's a different module!
isc_throw(CCSessionError, "Module name mismatch");
}
return (spec);
} else {
isc_throw(CCSessionError, "Error getting config for " + module_name + ": " + answer->str());
isc_throw(CCSessionError, "Error getting config for " +
module + ": " + answer->str());
}
}
ConfigData rmod_config = ConfigData(rmod_spec);
}
std::string
ModuleCCSession::addRemoteConfig(const std::string& spec_name,
void (*handler)(const std::string& module,
ConstElementPtr),
bool spec_is_filename)
{
// First get the module name, specification and default config
const ModuleSpec rmod_spec(fetchRemoteSpec(spec_name, spec_is_filename));
const std::string module_name(rmod_spec.getModuleName());
ConfigData rmod_config(rmod_spec);
// Get the current configuration values for that module
ConstElementPtr cmd = Element::fromJSON("{ \"command\": [\"get_config\", {\"module_name\":\"" + module_name + "\"} ] }");
unsigned int seq = session_.group_sendmsg(cmd, "ConfigManager");
// Get the current configuration values from config manager
ConstElementPtr cmd(createCommand("get_config",
Element::fromJSON("{\"module_name\": \"" +
module_name + "\"}")));
const unsigned int seq = session_.group_sendmsg(cmd, "ConfigManager");
ConstElementPtr env, answer;
session_.group_recvmsg(env, answer, false, seq);
......@@ -401,6 +427,7 @@ ModuleCCSession::addRemoteConfig(const std::string& spec_name,
ConstElementPtr new_config = parseAnswer(rcode, answer);
ElementPtr local_config;
if (rcode == 0 && new_config) {
// Merge the received config into existing local config
local_config = rmod_config.getLocalConfig();
isc::data::merge(local_config, new_config);
rmod_config.setLocalConfig(local_config);
......@@ -414,6 +441,8 @@ ModuleCCSession::addRemoteConfig(const std::string& spec_name,
remote_module_handlers_[module_name] = handler;
handler(module_name, local_config);
}
// Make sure we get updates in future
session_.subscribe(module_name);
return (module_name);
}
......
......@@ -171,6 +171,11 @@ public:
* @param command_handler A callback function pointer to be called when
* a control command from a remote agent needs to be performed on the
* local module.
* @start_immediately If true (default), start listening to new commands
* and configuration changes asynchronously at the end of the constructor;
* if false, it will be delayed until the start() method is explicitly
* called. (This is a short term workaround for an initialization trouble.
* We'll need to develop a cleaner solution, and then remove this knob)
*/
ModuleCCSession(const std::string& spec_file_name,
isc::cc::AbstractSession& session,
......@@ -178,9 +183,20 @@ public:
isc::data::ConstElementPtr new_config) = NULL,
isc::data::ConstElementPtr(*command_handler)(
const std::string& command,
isc::data::ConstElementPtr args) = NULL
isc::data::ConstElementPtr args) = NULL,
bool start_immediately = true
);
/// Start receiving new commands and configuration changes asynchronously.
///
/// This method must be called only once, and only when the ModuleCCSession
/// was constructed with start_immediately being false. Otherwise
/// CCSessionError will be thrown.
///
/// As noted in the constructor, this method should be considered a short
/// term workaround and will be removed in future.
void start();
/**
* Optional optimization for checkCommand loop; returns true
* if there are unhandled queued messages in the cc session.
......@@ -240,12 +256,16 @@ public:
* for those changes. This function will subscribe to the relevant module
* channel.
*
* This method must be called before calling the \c start() method on the
* ModuleCCSession (it also implies the ModuleCCSession must have been