Sample 11. channel name space management and security with
filter and translator
Based on applications name space management requirements, we may need
to "relocate"/"mount" the names imported (from a connection to a remote
name space) to a specific sub-region in name space. For example, if we
have a name space in desktop computer and connect to a PDA and a
laptop, we can set translators at connections so that names imported
from PDA will appear under "/pda/" and names from laptop will appear
under "/laptop/". Or if our application use integer as ids/names, we
may want to relocate ids from the 1st connection to [1000-1999] and ids
from next connection to [2000-2999] and so on. That is similar to the
way how we mount remote file-systems to local file-system.
Based on security requirements, we may need to use filters to restrict
the valid range of names allowed to pass in/out specific channel
connections. For example, a server's name space connect to 2 clients
and we want that these clients' name spaces and messaging are totally
separate, so that one client is unware of anything happening inside the
other client's name space such as new name-publications and message
passing. That is also similar to the way we protect network by
firewalls and NATs.
This sample uses a linear name space with strings as ids and implements
a server which can connect to multiple clients. For each connection,
the server will relocate the imported names to sub-region. For demo
purpose, we simply put the names from clients under "/client0/...",
"/client1/..."... and so on. So a name "basketball" in the first client
will appear in server's name space as "/client0/basketball".
First we define sample filter and translator in chat_defs.hpp.
When filter is created and bound for a connection, a prefix
string ( such as "/client0/", "/client1/" ) is passed in . Then this
filter will check name space change messages (init_subscription,
publication/unpublication, subscription/unsubscription) to block
names/ids which are not allowed; in our sample, valid names should
begin with correct prefix. both input and output directions will be
checked:
class filter_t :
public filter_type<string> {
public:
string
prefix;
filter_t(string pre) : prefix(pre) {}
bool
block_inward (string &id) {
if (id.find(prefix) != 0) {
cout << "id [" << id << "] blocked at inward<"
<< prefix << ">" << endl;
return true;
}
return false;
}
bool
block_outward (string &id) {
if (id.find(prefix) != 0) {
cout << "id [" << id << "] blocked at outward<"
<< prefix << ">" << endl;
return true;
}
return false;
}
};
When translator is created and bound for a connection, a prefix
string (
such as "/client0/", "/client1/" ) is passed in which designates where
imported names will appear. Then both incoming and outgoing messages
will be translated by the translator. In our simple demo, the
translation is adding and removing of the prefix:
class translator_t
: public translator_type<string> {
public:
string
prefix;
translator_t(string pre) : prefix(pre) {}
void
translate_inward (string & id) {
cout << "translate_inward : id[" << id << "] into [" ;
id = prefix + id;
cout << id << "]\n";
}
void
translate_outward (string & id) {
cout << "translate_outward : id[" << id << "] into ["
;
id.erase(0,prefix.size());
cout << id << "]\n";
}
};
A binder is just the combination of filter and translator.
class binder :
public binder_type<string> {
public:
binder(string prefix) {
filter = new filter_t(prefix);
translator = new translator_t(prefix);
}
};
Next lets see how filter and translator is set up in srv.cpp.
In srv.cpp, we define the following callback function, so that a proper
binder object will be created based on information about the connection
(here we use information about socket). In real applications, we may
need security provisions such as which range of ids/names are allowed
for which remote connections. Here we simply relocate names for remote
clients to "/client0/", "/client1/", etc.
binder *
binder_generator(boost::shared_ptr<tcp::socket> sock)
{
static int
num = 0;
string
prefix = "/client";
std::ostringstream os;
os
<< num;
num++;
return
new binder(prefix+os.str()+"/");
}
Next in srv.cpp when we accept connection from clients,
binder_generator is passed in so that proper binder will be created and
used for each new connection:
connector.async_accept(atoi(srv_port), // channel address
boost::bind(asio_bind_sock_chan<chat_chan,
chat_chan::text_marshaler_registry>(&binder_generator),
boost::ref(chan),
boost::ref(mar_reg), _1, _2,_3));
A sample session could be as following:
1. start server
srv 8888
2. start first client (test1)
cli test1 localhost 8888
3. start 2nd client (test2)
cli test2 localhost 8888
4. in test1, add a new id/name such as "fishing"; from server's log,
we'll see it appears in server's namespace as "/client0/fishing";
in test1, we can use "fishing" to send message to
server, while at server we need to use "/client0/fishing" to send
message to
test1.
5. in test2, add a new id/name such as "reading"; from server's log,
we'll see it appears in server's namespace as "/client1/reading";
in test2, we can use "reading" to send message to
server, while at
server we need to use "/client1/reading" to send message to
test2.
6. notice that test1 is not aware of what is happening with test2 and
vice versa.
Complete source code listing:
chat_defs.hpp
cli.cpp
srv.cpp