|
|
Developers
Greetings, fellow developers!
Moxy was written with the idea that extending it's basic
functionality should be easy, especially in terms of logging
and domain lookups. Here you will find a description of the
project layout as well as some specific tips to help you make
Moxy do what you want.
I have put a number of hours into this project, using the
Psychogenic.com resources, and have released the code under the GNU
GPL. Please contact me and send me your modifications
so that the project may improve and we all can profit.
General layout
Moxy was written in C++. As described on the home page, it
runs as a daemon listening for connections and forks a process
for incoming connections, which handles the entire transaction
with the sender.
Here's a simplified graphical description of the various
Moxy components
Note that not all classes, relationships, methods or parameters
have been preserved in order to get a semblance of clarity
in 640 pixels. As you can see, all the log subclasses implement
a common interface inherited from the (abstract base) Log
class. The same holds for the DomainDB subclasses. Extending
the Moxy logging or lookup facilities is therefore a simple
matter of creating an appropriate subclass. Of course there
are some extra details involved (like instantiating the new
class in the driver) that I shall describe here.
Adding a new domain database type
The DomainDB derived objects used to lookup domains and translate
them to a list of destinations (hosts and ports) all share
a common interface. To implement a new DB type, for instance
one that looks up the domains in a postgres table, follow
these 4 steps.
-
Derive a new class from DomainDB (eg PostgresDB). Implement
all the DomainDB methods (open(), close(), etc) in a manner
that reflects the type of db you are dealing with. The
method that you'll probably need to focus on most is:
bool get(const string & domain, DestList & returnList);
In the get() method, do your magic to find the
hosts (or IPs) and ports that match the domain string. For each of these host/port combos, simply call
returnList.add(destHost, destPort);
(part of the DestList interface found in destlist.hh).
If at least one host/port combination is found
for the domain, return true, else return false.
-
Define your DBTYPE in domaindb.hh (eg DBTYPE_PGRES)
and include your header file (eg pgresdb.hh)
in main.cc.
Also in main.cc, modify open_new_DB() so it will instantiate an appropriate DB object when configured
for you new type.
-
Make your additions portable. Please give your new class
some thought so that others will be able to use it without
modifying the source code. For instance, instead of hard
coding:
"SELECT host,port from clienttable where domain='%s'"
in your PostgresDB::get() implementation, use
something like:
"SELECT %s from %s where %s"
and fill in the query using variables from the config
file. Adding a value to the config file is a simple as
inserting lines in the .conf, so inserting:
dbselect "host, port"
dbtable clienttable
dbwhere "domain='%domain'"
and using these options in your code (see config.hh for methods used to fetch configuration parameters) should
be very easy. A note on the example dbwhere above: in
order to ensure maximum flexibility, a scheme using magic
substrings (like %domain) was implemented. It
would be nice if a similar technique could be used in
cases like this one, so have a look at the SysLog (syslog.cc)
or FileLog (filelog.cc) log() implementations.
Both use the utils SearchReplace() function
to allow users to set the exact string formatting for
SysLog and Filelog logs and are quite straightforward.
-
Test your code then make sure you send it back to us
so it can be incorporated into the main Moxy tree. This
will avoid code forks, allowing you to upgrade later without
having to reintegrate your additions, and allow others
to benefit as well.
You may add debugging output throughout your implementation
by enclosing it with DBDEBUG conditionals, like:
#ifdef DBDEBUG
cerr << "Creating new PostgresDB object." << endl;
#endif
Take a look at the domaindb.hh and the other *db.hh and *db.cc files before you start.
Adding a new logging facility
The Log class derived objects which are used to log bandwidth
usage all share a common interface. The procedure to follow
to implement a new logging facility (eg one that updates a
MySQL database) involves little more than creating a new class
derived from Log and resembles the method used to add a new
lookup database described above. To do so:
-
Derive a new class from Log (eg MysqlLog). Implement
all the Log methods (open(), close(), log())
in a manner appropriate to your circumstances. Most of
the code will be spent implementing the log() methods:
bool log(const string & message);
bool log(const string & senderHost, const string & senderEmail,
const string & destHost, const string & recipients, long msgsize);
If logging arbitrary strings is not applicable to your
type (as in the case of a MySQL logging facility), you
may safely ignore the log(const string & message) method by simply returning true and dropping
the message.
The other log() method needs to be implemented
in a manner consistent with your facility. It is called
by the MTA after sending mail to a host. The function
parameters are:
senderHost The sender's IP (eg "216.182.45.14")
senderEmail The sender's email address (eg "pat@hisdomain.com")
destHost The host (or IP) as listed in the lookup db and the port number
(eg "psychogenic.com:3500")
recipients A comma delimited list of the recipient email addresses for
this domain (eg "bill@microsoft.com,ralph@msrulez.com")
msgsize The length of the message, in bytes (eg 244302)
and you may use any of these in your logs, the two most
important being destHost and msgsize.
-
Define you new LOGTYPE in log.hh (eg
LOGTYPE_MYSQL) and include your header file (mysqllog.hh)
in main.cc. Also in main.cc, modify
open_new_Log() so that it will instantiate an appropriate
Log object when configured for you new type.
-
Make your additions portable. Please give your new class
some thought so that others will be able to use it without
modifying the source code. For instance, instead of hard
coding:
"INSERT INTO bwtable (host, size) VALUES(%s,
%s)"
in your MysqlLog::log() implementation, use something
like:
"INSERT INTO %s (%s) VALUES(%s)"
and fill in the query using variables from the config
file. Adding a value to the config file is a simple as
inserting lines in the .conf, so inserting:
logTable bwtable
logTableCols "host, size"
logTableVals "%dhost, %msize"
and using these options in your code (see config.hh for methods used to fetch configuration parameters) should
be very easy. A note on the example logTableVals above: in order to ensure maximum flexibility, a scheme
using magic substrings (like %dhost) was implemented.
It would be nice if a similar technique could be used
in cases like this one, so have a look at the SysLog (syslog.cc)
or FileLog (filelog.cc) implementations. Both
use the utils SearchReplace() function
to allow users to set the exact string formatting for
SysLog and Filelog logs and are quite straightforward.
-
Test your code then make sure you send it back to us
so it can be incorporated into the main Moxy tree. This
will avoid code forks, allowing you to upgrade later without
having to reintegrate your additions, and allow others
to benefit as well.
You may add debugging output throughout your implementation
by enclosing it with LOGDEBUG conditionals, like:
#ifdef LOGDEBUG
cerr << "MysqlLog opening " << dbname << endl;
#endif
Take a look at the log.hh and the other *log.hh and *log.cc files before you start.
Notes on using the Config class
The Config class is used extensively in Moxy. To get access
to the program configuration in your class implementations,
simply add
#include "config.hh"
extern Config MyConf;
to your .cc file. Yes, I know, it's an ugly global but it
was the best way for everyone to have access to the configuration
without forcing classes to pass around unnecessary parameters
all the time and, anyway, the configuration is (logically)
global in scope - there is only one config per program.
Take a look at config.hh to see the class' interface.
All you normally need to worry about is the
string get(string attrib, const string & default = "")
member function. attrib is the attribute you are
requesting and default is (an optional) default value
to return if attrib is not found. The get() returns a string containing the value found in the configuration
file or an empty string ("") if the parameter was
not present and default was not set.
Note that the Config class may be set to be case insensitive
or not but, in Moxy, the parameters names (the key values,
attribs) are case insensitive. In the
config file, dbname, DBName and DbNamE are all equivalent - so when calling get(), use the
all lowercase version. The values returned by Config::get() are case sensitive in the sense that their case is
not affected by the class - the values are returned as they
are found in the configuration file.
Debugging
When first testing your installation, you might want to try starting moxy in a terminal without the -d option
so you will see it log any errors. If you really need to debug moxy get the source code and define
DEBUG (-DDEBUG) while compiling -- you will get lotsa output. You might want to check the defaults.hh file if you are
debugging something specific and only want that module to be verbose, there are many flags you can set such as CONFDEBUG, LOGDEBUG and
OPTDEBUG.
|
|
|