move all code into /trunk

This commit is contained in:
ps 2008-12-14 19:12:48 +00:00
commit 2aac3e1e88
42 changed files with 6888 additions and 0 deletions

172
src/Configfile.cpp.in Normal file
View file

@ -0,0 +1,172 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#include "Configfile.h"
/**
* default config
*
*/
Configfile::Configfile()
{
%templ_default_values%
}
void Configfile::parse(string file)
{
ifstream f;
char line[255];
int equalpos;
f.open(file.c_str(),ios::in);
while(!f.eof())
{
f.getline(line,255);
string l=Utils::trim(line);
if('#'!=l[0]&&l!=""&&l.find("="))
{
equalpos=l.find("=");
string option=Utils::trim(l.substr(0,equalpos));
string value=Utils::trim(l.substr(equalpos+1));
#ifdef REALLY_VERBOSE_DEBUG
cout << l << endl;
cout << option << "->" << value << endl;
#endif //REALLY_VERBOSE_DEBUG
//this is a bit of a hack, but simplifies a lot this function
#define PARSE_INT(x,y) if(x==option) y=Configfile::parseAsInt(value); else
#define PARSE_BOOL(x,y) if(x==option) y=Configfile::parseAsBool(value); else
#define PARSE_STRING(x,y) if(x==option) y=Configfile::parseAsString(value); else
#define PARSE_LIST(x,y) if(x==option) y=Configfile::parseAsList(value); else
%templ_parsevars%
{
throw Exception("Option \""+option+"\" with value \""+value+"\" is not recognized",__FILE__,__LINE__);
}
#undef PARSE_INT
#undef PARSE_BOOL
#undef PARSE_STRING
#undef PARSE_LIST
}
}
#ifndef WIN32
uid=Utils::usertouid(user);
gid=Utils::grouptogid(group);
#endif //WIN32
f.close();
}
//again, this is a BIG HACK, but it simplifies code a lot
#define GET_VAR(x,y,z) z Configfile::x(){ return y;}
GET_VAR(getUid,uid,int)
GET_VAR(getGid,gid,int)
%templ_getmethods%
#undef GET_VAR
void Configfile::validateConfig()
{
#ifndef WIN32
//check if we are root if we want to bind to a port lower than 1024
if(getuid()!=0&&listening_port<1024)
throw Exception(_("You can't bind to a port lower than 1024 without being root"),__FILE__,__LINE__);
#endif //WIN32
#ifdef HAVE_SSL
//check if ssl is usable
if(!Utils::file_exists(certificate_file))
throw Exception("Certificate file "+certificate_file+" doesn't exist.\nTo generate a certificate look in hermesrc.example, there is an example there.",__FILE__,__LINE__);
if(!Utils::file_exists(private_key_file))
throw Exception("Private key file "+private_key_file+" doesn't exist.\nTo generate a private key look in hermesrc.example, there is an example there.",__FILE__,__LINE__);
#endif //HAVE_SSL
#ifndef WIN32
//check if chroot dir exist //TODO: check that files needed in chroot exist
//for now only /etc/resolv.conf, but we're working on it :-D
if(""!=chroot&&!Utils::dir_exists(chroot))
throw Exception("Directory "+chroot+" doesn't exist, can't chroot to it.",__FILE__,__LINE__);
#endif //WIN32
//check if we have submit_stats on but no user and password
if(getSubmitStats()&&(""==getSubmitStatsUsername()||""==getSubmitStatsPassword()))
throw Exception("You have configured hermes to send stats, but have not configured a username or password.\n"
"If you don't have one, go to http://www.hermes-project.com and register there",__FILE__,__LINE__);
#ifndef HAVE_SSL
//check if we have activated submit_stats_ssl not having ssl activated
if(getSubmitStatsSsl())
throw Exception("You have configured stats submission through SSL, but hermes was compiled without SSL support",__FILE__,__LINE__);
#endif //HAVE_SSL
}
string Configfile::parseAsString(string str)
{
//remove "" round the string
if('"'==str[0])
str=str.substr(1);
if('"'==str[str.length()-1])
str=str.substr(0,str.length()-1);
return str;
}
bool Configfile::parseAsBool(string str)
{
if("yes"==str||"on"==str||"1"==str||"true"==str)
return true;
else
return false;
}
long int Configfile::parseAsInt(string str)
{
long int value;
errno=0; //to know why we do this, read NOTES on strtol(3)
value=strtol(str.c_str(),NULL,10);
if(errno)
throw Exception("Error parsing as int ("+Utils::errnotostrerror(errno)+")",__FILE__,__LINE__);
return value;
}
list<string> Configfile::parseAsList(string str)
{
list<string> tmpList;
string::size_type startpos=0,endpos=0,len;
string tmpstr;
str=Configfile::parseAsString(str); //remove quotes around string
len=str.length();
while(startpos<len&&string::npos!=endpos)
{
endpos=str.find(',',startpos);
if(string::npos==endpos)
tmpstr=str.substr(startpos);
else
tmpstr=str.substr(startpos,endpos-startpos);
tmpList.push_back(Utils::trim(tmpstr));
startpos=endpos+1;
}
return tmpList;
}

52
src/Configfile.h.in Normal file
View file

@ -0,0 +1,52 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#ifndef CONFIGFILE_H
#define CONFIGFILE_H
#include "hermes.h"
#include <list>
#include <string>
#include <iostream>
#include <fstream>
#include "Utils.h"
using namespace std;
class Configfile
{
private:
static string parseAsString(string);
static bool parseAsBool(string);
static long parseAsInt(string);
static list<string> parseAsList(string);
int uid;
int gid;
%templ_privateattribs%
public:
Configfile();
void parse(string);
void validateConfig();
int getUid();
int getGid();
%templ_publicmethods%
};
#endif //CONFIGFILE_H

253
src/Configfile.tmpl Normal file
View file

@ -0,0 +1,253 @@
#include "../config.h"
* comments MUST begin with a #.
*
* boolean options recognise (true, 1, on, yes) as true
* and anything else as false
*
* string literals can be surrounded by the " character,
* but everything else CANNOT be
*
*clean*
#ifndef WIN32
* whether to fork to the background. initscripts require
* this to be true most of the time.
bool,background,true
* chroot to this directory on startup.
* this path is ABSOLUTE, it WON'T work with a relative path,
* because we are chrooting to the dir BEFORE chrooting, as a
* security measure.
* to disable chrooting, use an empty string (default).
string,chroot,""
* drop privileges once running? recomended.
bool,drop_privileges,true
* user to drop privileges to.
string,user,"nobody"
* group to drop privileges to.
string,group,"nobody"
* write a pid file with the pid of the main hermes server.
* if you set background=true above, this will write the pid
* of the forked hermes, not the original.
string,pid_file,"/var/run/hermes.pid"
#endif //WIN32
* the port where hermes will listen for new connection.
* if you are going to use a port lower than 1024 (almost always,
* smtp is 25, smtps is 465 and delivery is 587), then you need
* to run as root (you can drop privileges) or with setUID active.
int,listening_port,25
* the ip to bind to. if you leave it empty (default), then it
* listens on all available ips
string,bind_to,""
* the host of the real smtp server.
* if your server is qmail and you have the AUTH patch,
* DON'T use localhost, use the external IP instead.
string,server_host,"localhost"
* the port for the real smtp server.
int,server_port,2525
* database file to use.
* if you are chrooting, the path is relative to the chroot:
* real filepath = chroot + database_file
#ifdef WIN32
string,database_file,"greylisting.db"
#else
string,database_file,"/var/hermes/greylisting.db"
#endif //WIN32
* whether to use greylisting.
* greylisting will slightly delay your emails (configurable, see below)
* to stop most spam. is the most efective technique in use by hermes.
bool,greylist,true
* whether to throttle connection.
* it will force some spammers (the more impatient ones) to drop the connection
* and leave you alone.
bool,throttle,true
* throttling time
* this is the time (in seconds) that hermes will wait between each sent line.
* don't set this too high (more than 3), as that will drop MANY connections
int,throttling_time,1
* whether we should check if there is data before we send the SMTP banner.
* if there is data the email is almost certainly spam.
bool,allow_data_before_banner,false
* dns blacklist domain list to check.
* if this is empty (default) hermes will not check anything, effectively disabling
* dns blacklisting.
* recommended value is "zen.spamhaus.org"
list,dns_blacklist_domains,""
* percentage of domains that have to blacklist an ip before considering it blacklisted.
* for example if you need a domain to be listed in only half of the blacklists to be considered
* as listed, just define dns_blacklist_percentage as 50 (50%)
int,dns_blacklist_percentage,100
* dns whitelist domain to check.
* if this is empty (default) hermes will not check anything, effectively disabling
* dns whitelisting.
* this lists should only list hosts that have a history of NOT sending spam.
* recommended value is "list.dnswl.org"
list,dns_whitelist_domains,""
* percentage of domains that have to whitelist an ip before considering it whitelisted.
* for example if you need a domain to be listed in only half of the whitelists to be considered
* as listed, just define dns_whitelist_percentage as 50 (50%).
int,dns_whitelist_percentage,100
* time to delay the initial SMTP banner
int,banner_delay_time,5
#ifdef REALLY_VERBOSE_DEBUG
* email to notify exceptions to.
* CAVEAT: the code that does this is VERY BUGGY and VERY VERBOSE, don't use unless you
* are a developer looking for a bug.
string,notify_to,""
#endif //REALLY_VERBOSE_DEBUG
* greylisting options.
*
*clean*
* initial expiry time.
* when email is first recorded, it will expire after this time (in minutes).
int,initial_expiry,240
* initial period of time (in minutes) during which a retry on the spammer's side will FAIL.
int,initial_blacklist,5
* once we have whitelisted a triplet, how long it stays whitelisted (in days).
* 36 is a magic number, is the maximum days between a day and the same day next month
int,whitelist_expiry,36
* whether to submit stats.
bool,submit_stats,true
* should stats be submited using SSL?
* recomended, but some people will compile without ssl.
#ifdef HAVE_SSL
bool,submit_stats_ssl,true
#else
bool,submit_stats_ssl,false
#endif //HAVE_SSL
* username (used to submit stats).
* you can register on http://www.hermes-project.com
string,submit_stats_username,"anonymous"
* password
string,submit_stats_password,"anonymous"
#if LOGGER_CLASS==FileLogger
* if you are using the filelogger, which file to log to.
string,file_logger_filename,"hermes.log"
* whether to keep the logger file locked between writes
bool,keep_file_locked,true
* frequency for log rotating in minutes
* default is 1440 (1 day)
* 0 means no rotation
int,log_rotation_frequency,1440
* format for the logfile rotation
* if you are using logfile rotation, file_logger represents the filename
* to which the logger will write, while this is the name files will get
* when rotated
* you can use the following variables:
* %%year%% - current year (4 digits)
* %%month%% - current month
* %%day%% - current day
* %%hour%% - current hour
* %%minute%% - current minute
* all of them are zero-padded
string,rotate_filename,"hermes-%%year%%-%%month%%-%%day%%-%%hour%%:%%minute%%.log"
#endif //LOGGER_CLASS==FileLogger
* whether to clean the database file and send stats.
* if you have two instances of hermes running (for example one for smtp and other for smtps)
* you want to configure all of them but one to use clean_db=false.
* also, you might prefer to not clean the database at all for many reasons (for example to
* keep a huge file around with all your system's email data).
* anyway, this doesn't mean in anyway that entries in the database won't expire, only that they
* will be left hanging around without any use.
bool,clean_db,true
#ifdef HAVE_SSL
* ssl-related config options
* NOTE: this NEEDS the openssl library
*
*clean*
* should we try to connect to our real smtp server using ssl?
* not really neccesary unless real smtp server is on other machine.
bool,outgoing_ssl,false
* should we accept connections using ssl?
* NOTE: this does NOT disable the starttls capability, only starts hermes expecting SSL negotiation.
* starttls is handled the following way: if you have ssl, it is always on. clients can request it at
* any time an hermes will change to ssl at once. if you don't have ssl, hermes will refuse to starttls
* with a 354 error code, although it WILL still accept the command. connection should proceed normally
* even on that event
bool,incoming_ssl,false
* file with our private key (PEM format).
* to generate, execute:
* # openssl genrsa 1024 > private.key
string,private_key_file,"/etc/hermes/hermes.key"
* file with our server certificate (PEM format).
* to generate, execute:
* # openssl req -new -x509 -nodes -sha1 -days 365 -key private.key > certificate.crt
* and answer the questions
string,certificate_file,"/etc/hermes/hermes.cert"
#endif //HAVE_SSL
* whether to add headers to the email sent or no.
* to be rfc compatible this HAS to be true, but if you set to false, no one will know you are using hermes
bool,add_headers,true
* the hostname to use for the headers. useful only in case that gethostname() returns
* something that is not correct. For example on windows, it seems to return only the host
* part of the name.
*
* if this is empty, hermes will use the value returned by gethostname()
string,hostname,""
* should a whitelisted hostname or whitelisted ip also disable throttling and banner delaying?
* it is useful to make remote hosts deliver mail almost at once
bool,whitelisted_disables_everything,true
* whether to reject connections from hosts that do not provide DNS reverse resolution.
* don't enable if you don't know what you are doing or what this switch does
bool,reject_no_reverse_resolution,false
* check whether your ehlo hostname matches your ip reverse resolution.
* don't enable unless you understand perfectly what this means
bool,check_helo_against_reverse,false
* whether to query the spf record for the incoming domain.
* should help, enable if you have libspf (if you don't, install it and recompile)
#ifdef HAVE_SPF
bool,query_spf,true
#else
bool,query_spf,false
#endif //HAVE_SPF
* return temporary error instead of permanent error.
* Currently, this only applies to SPF and DNSBL rejected email
* You should enable this while debugging your hermes installation,
* as configuration errors won't be fatal.
bool,return_temp_error_on_reject,false

427
src/Database.cpp Normal file
View file

@ -0,0 +1,427 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#include "Database.h"
void Database::setDatabaseFile(string p_dbfile)
{
dbfile=p_dbfile;
}
/**
*
* this function executes a query and checks for error
* it doesn't return any value, as it is not needed(the queries don't return data)
*
*/
void Database::doQuery(string p_sql)
{
int retval;
do
{
retval=sqlite3_exec(dbh,p_sql.c_str(),NULL,NULL,NULL);
if(SQLITE_OK!=retval&&SQLITE_BUSY!=retval)
throw SQLException("SQL: "+p_sql+" sqlite3_errmsg: "+sqlite3_errmsg(dbh),__FILE__,__LINE__);
if(SQLITE_BUSY==retval)
{
sleep(1+rand()%2);
#ifdef REALLY_VERBOSE_DEBUG
cout << pthread_self() << " doquery() sql failed with busy state, retrying" << endl;
#endif //REALLY_VERBOSE_DEBUG
}
}
while(SQLITE_BUSY==retval);
}
string Database::cleanString(string s)
{
string result="";
for(unsigned int i=0;i<s.length();i++)
if(s[i]>31&&s[i]<127)
switch(s[i])
{
case ' ':
case '<':
case '>':
case '(':
case ')':
case '[':
case ']':
case '\\':
case ',':
case ';':
case ':':
case '"':
case '%':
case '\'':
break;
default:
result+=s[i];
}
return result;
}
bool Database::greylisted(string ip,string from,string to,int initial_expiry,int initial_blacklist,int whitelist_expiry)
{
char **result;
int nrow=0;
int ncolumn=0;
bool retval=true;
int sqlite_retval;
int now=time(NULL);
string strnow=Utils::inttostr(now);
string sql="SELECT id,blocked_until FROM greylist WHERE ip=\""+ip+"\" AND emailfrom=\""+from+"\" AND emailto=\""+to+"\" AND "+strnow+" < expires LIMIT 1;";
do
{
sqlite_retval=sqlite3_get_table(dbh,sql.c_str(),&result,&nrow,&ncolumn,NULL);
if(sqlite_retval!=SQLITE_OK&&sqlite_retval!=SQLITE_BUSY)
{
if(NULL!=result)
sqlite3_free_table(result);
throw SQLException("SQL: "+sql+" sqlite3_errmsg: "+sqlite3_errmsg(dbh),__FILE__,__LINE__);
}
if(SQLITE_BUSY==sqlite_retval)
{
sleep(1+rand()%2);
#ifdef REALLY_VERBOSE_DEBUG
cout << pthread_self() << " greylisted() sql busy" << endl;
#endif //REALLY_VERBOSE_DEBUG
}
}
while(sqlite_retval==SQLITE_BUSY);
sql="";
if(nrow>0)
{
string id=result[2];
//we have seen this triplet before
if(now<atol(result[3]))
{
sql="UPDATE greylist SET blocked=blocked+1 WHERE id="+id+";";
doQuery(sql.c_str());
retval=true;
}
else
{
string expires=Utils::inttostr(now+(60*60*24*whitelist_expiry));
sql="UPDATE greylist SET expires="+expires+",passed=passed+1 WHERE id="+id+";";
doQuery(sql.c_str());
retval=false;
}
}
else
{
string blocked_until=Utils::inttostr(now+(60*initial_blacklist));
string expires=Utils::inttostr(now+(60*initial_expiry));
//new triplet, greylist and add new row
retval=true;
sql="INSERT INTO greylist(id,ip,emailfrom,emailto,created,blocked_until,expires,passed,blocked)"
"VALUES(NULL,\""+ip+"\",\""+from+"\",\""+to+"\","+strnow+","+blocked_until+","+expires+",0,1);";
doQuery(sql.c_str());
}
if(NULL!=result)
sqlite3_free_table(result);
return retval;
}
Database::Database():dbh(NULL)
{}
Database::~Database()
{
close();
}
void Database::close()
{
if(NULL!=dbh)
sqlite3_close(dbh);
}
void Database::_open()
{
if(sqlite3_open(dbfile.c_str(),&dbh))
{
dbh=NULL;
throw Exception(_("Error creating/opening db ")+dbfile,__FILE__,__LINE__);
}
}
void Database::open()
{
// if dbfile is new, initialize first
if(!Utils::file_exists(dbfile))
init();
_open();
}
void Database::init()
{
_open();
doQuery("CREATE TABLE whitelisted_ips(ip VARCHAR);");
doQuery("CREATE TABLE whitelisted_tos(email VARCHAR);");
doQuery("CREATE TABLE whitelisted_domains(domain VARCHAR);");
doQuery("CREATE TABLE whitelisted_hostnames(hostname VARCHAR);");
doQuery("CREATE TABLE blacklisted_tos(email VARCHAR);");
doQuery("CREATE TABLE blacklisted_todomains(domain VARCHAR);");
doQuery("CREATE TABLE blacklisted_ips(ip VARCHAR);");
doQuery("CREATE TABLE blacklisted_froms(email VARCHAR);");
doQuery("CREATE TABLE allowed_domains_per_ip(domain VARCHAR,ip VARCHAR);");
doQuery("CREATE TABLE greylist(id INTEGER PRIMARY KEY,ip VARCHAR,emailfrom VARCHAR,emailto VARCHAR,created INTEGER,blocked_until INTEGER,expires INTEGER,passed INTEGER,blocked INTEGER);");
//whitelist localhost
doQuery("INSERT INTO whitelisted_ips(ip) VALUES(\"127.0.0.1\");");
close();
}
int Database::countRows(string p_sql)
{
char **result;
int nrow=0;
int ncolumn=0;
int sqlite_retval;
do
{
sqlite_retval=sqlite3_get_table(dbh,p_sql.c_str(),&result,&nrow,&ncolumn,NULL);
if(SQLITE_OK!=sqlite_retval&&SQLITE_BUSY!=sqlite_retval)
{
if(NULL!=result)
sqlite3_free_table(result);
throw SQLException("SQL: "+p_sql+" sqlite3_errmsg: "+sqlite3_errmsg(dbh),__FILE__,__LINE__);
}
if(SQLITE_BUSY==sqlite_retval)
{
sleep(1+rand()%2);
#ifdef REALLY_VERBOSE_DEBUG
cout << pthread_self() << " countRows() retrying" << endl;
#endif //REALLY_VERBOSE_DEBUG
}
}
while(SQLITE_BUSY==sqlite_retval);
if(NULL!=result)
sqlite3_free_table(result);
if(ncolumn)
return (nrow/ncolumn);
else
return nrow;
}
bool Database::whitelistedIP(string p_ip)
{
string sql="";
sql="SELECT ip FROM whitelisted_ips WHERE ip=SUBSTR(\""+p_ip+"\",0,LENGTH(ip)) LIMIT 1;";
if(countRows(sql)>0)
return true;
else
return false;
}
bool Database::whitelistedTO(string p_email)
{
string sql="";
sql="SELECT email FROM whitelisted_tos WHERE email=\""+p_email+"\" LIMIT 1;";
if(countRows(sql)>0)
return true;
else
return false;
}
bool Database::whitelistedDomain(string p_domain)
{
string sql="";
sql="SELECT domain FROM whitelisted_domains WHERE domain=\""+p_domain+"\" LIMIT 1;";
if(countRows(sql)>0)
return true;
else
return false;
}
bool Database::blacklistedTO(string p_email)
{
string sql="";
sql="SELECT email FROM blacklisted_tos WHERE email=\""+p_email+"\" LIMIT 1;";
if(countRows(sql)>0)
return true;
else
return false;
}
bool Database::blacklistedToDomain(string p_domain)
{
string sql="";
sql="SELECT domain FROM blacklisted_todomains WHERE domain=\""+p_domain+"\" LIMIT 1;";
if(countRows(sql)>0)
return true;
else
return false;
}
bool Database::blacklistedIP(string p_ip)
{
string sql="";
sql="SELECT ip FROM blacklisted_ips WHERE ip=SUBSTR(\""+p_ip+"\",0,LENGTH(ip)) LIMIT 1;";
if(countRows(sql)>0)
return true;
else
return false;
}
bool Database::blacklistedFROM(string p_email)
{
string sql="";
sql="SELECT email FROM blacklisted_froms WHERE email=\""+p_email+"\" LIMIT 1;";
if(countRows(sql)>0)
return true;
else
return false;
}
bool Database::whitelistedHostname(string p_hostname)
{
string sql="";
sql="SELECT hostname FROM whitelisted_hostnames WHERE hostname=SUBSTR(\""+p_hostname+"\",-LENGTH(hostname),LENGTH(hostname)) LIMIT 1;";
if(countRows(sql)>0)
return true;
else
return false;
}
bool Database::allowedDomainPerIP(string p_domain,string p_ip)
{
string sql="",sql_domain="";
sql="SELECT ip FROM allowed_domains_per_ip WHERE domain=\""+p_domain+"\" AND ip=\""+p_ip+"\" LIMIT 1;";
sql_domain="SELECT ip FROM allowed_domains_per_ip WHERE domain=\""+p_domain+"\" LIMIT 1;";
if(countRows(sql_domain)>0&&0==countRows(sql))
return false;
else
return true;
}
/**
* this function returns an integer value from a sql
* it is useful to calculate things with sql
*
* i.e.: SELECT SUM(intfield) FROM table
*
* @param p_sql SQL query to perform
*
* @return the first value of the first column, rest of data is ignored
*/
unsigned long Database::getIntValue(string& p_sql)
{
char **result;
int nrow=0;
int ncolumn=0;
int sqlite_retval;
unsigned long value;
do
{
sqlite_retval=sqlite3_get_table(dbh,p_sql.c_str(),&result,&nrow,&ncolumn,NULL);
if(SQLITE_OK!=sqlite_retval&&SQLITE_BUSY!=sqlite_retval)
{
if(NULL!=result)
sqlite3_free_table(result);
throw SQLException("SQL: "+p_sql+" sqlite3_errmsg: "+sqlite3_errmsg(dbh),__FILE__,__LINE__);
}
if(SQLITE_BUSY==sqlite_retval)
{
sleep(1+rand()%2);
#ifdef REALLY_VERBOSE_DEBUG
cout << pthread_self() << " getIntValue() retrying" << endl;
#endif //REALLY_VERBOSE_DEBUG
}
}
while(SQLITE_BUSY==sqlite_retval);
if(NULL==result)
throw SQLException("SQL: "+p_sql+" didn't return any data, SQL query may be wrong",__FILE__,__LINE__);
if('\0'==result[ncolumn])
value=0; //why sqlite doesn't return 0 when there are no rows?
else
value=strtoul(result[ncolumn],NULL,10);
sqlite3_free_table(result);
return value;
}
/**
* clean the spam database and return the number of spam messages
*
* @return number of spam messages deleted
*/
unsigned long Database::cleanDB()
{
unsigned long spamcount=0; //shut compiler up
string sql;
try
{
//block database until we have finished cleaning it
doQuery("BEGIN EXCLUSIVE TRANSACTION");
//now count how many blocked emails we have to submit to stats
//we do it always because if we don't submit stats it stills appears on the logs
sql="SELECT SUM(blocked) FROM greylist WHERE expires<strftime('%s','now') AND passed=0;";
spamcount=getIntValue(sql);
#ifdef REALLY_VERBOSE_DEBUG
cout << "We have processed " << spamcount << " spam emails in the last 4 hours" << endl;
#endif //REALLY_VERBOSE_DEBUG
//at last, delete them from the database
doQuery("DELETE FROM greylist WHERE expires<strftime('%s','now');");
//and close the transaction
doQuery("COMMIT TRANSACTION");
}
catch(Exception &e)
{
doQuery("ROLLBACK TRANSACTION");
}
return spamcount;
}

67
src/Database.h Normal file
View file

@ -0,0 +1,67 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#ifndef DATABASE_H
#define DATABASE_H
#include "hermes.h"
#include <iostream>
#include <string>
#include <sqlite3.h>
#include <pthread.h>
#include "Utils.h"
using namespace std;
/**
* this class implements an interface to the sqlite3 database
*/
class Database
{
private:
string dbfile;
sqlite3 *dbh;
void _open();
int countRows(string);
void doQuery(string);
unsigned long getIntValue(string&);
public:
Database();
~Database();
void setDatabaseFile(string);
static string cleanString(string);
void init();
void open();
void close();
bool greylisted(string,string,string,int,int,int);
bool whitelistedIP(string);
bool whitelistedHostname(string);
bool whitelistedTO(string);
bool whitelistedDomain(string);
bool blacklistedTO(string);
bool blacklistedToDomain(string);
bool blacklistedIP(string);
bool blacklistedFROM(string);
bool allowedDomainPerIP(string,string);
unsigned long cleanDB();
};
#endif //DATABASE_H

146
src/Exception.cpp Normal file
View file

@ -0,0 +1,146 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#include "Exception.h"
extern Configfile cfg;
extern LOGGER_CLASS hermes_log;
Exception::Exception(string p_error,string p_file,unsigned p_line)
{
error=p_error;
#ifdef REALLY_VERBOSE_DEBUG
hermes_log.addMessage(LOG_ERR,p_error);
#endif //REALLY_VERBOSE_DEBUG
#ifdef NOTIFY_EXCEPTIONS
if(cfg.getNotifyTo()!="")
{
try
{
notifyByEmail("Exception: "+p_error);
}
catch(NotifyException &e)
{
cout << string(e) << endl;
}
}
#endif //NOTIFY_EXCEPTIONS
file=p_file;
line=p_line;
}
Exception::operator string()
{
return file+":"+Utils::inttostr(line)+": "+error;
}
ostream& operator<<(ostream &os,Exception &e)
{
os << string(e);
return os;
}
#ifdef NOTIFY_EXCEPTIONS
#ifdef USE_SMTP_FOR_NOTIFICATIONS
/**
* notify problem by email to address, using an smtp server
*
* it will add some text before the message to be identificable
* this function only makes mild error-checks on codes returned
* by server, so some email might not reach destination
* this functions' mission in life is help those poor souls still
* using windows(and others), as they just don't have sendmail or popen,
* wich is way faster, way less complicated and much less error-prone
* also if you're using chroot, you might want to use smtp instead of sendmail
* as it is a much simpler setup
*
* @param string message message of the problem
*
* @return void
*/
void Exception::notifyByEmail(string message)
{
Socket smtpServer;
string address=cfg.getNotifyTo();
string response;
if(""==address)
throw Exception(_("Notification address is incorrect or empty, please check"),__FILE__,__LINE__);
smtpServer.init();
smtpServer.connect(cfg.getServerHost(),cfg.getServerPort());
#ifdef HAVE_SSL
if(cfg.getOutgoingSSL())
smtpServer.enableSSL(false);
#endif //HAVE_SSL
#define ERROR_SENDING(x,y,z) if(x.substr(0,3)!=y) throw NotifyException(_("Error "+x.substr(0,3)+" sending email after ")+z);
#define GET_RESPONSE_AND_CHECK(x,y) response=smtpServer.readLine(); ERROR_SENDING(response,x,y); cout << "s:" << response << endl;
#define SEND_COMMAND_AND_CHECK(x,y,z) smtpServer.writeLine(x); cout << "c:" << x << endl; GET_RESPONSE_AND_CHECK(y,z);
#define DEBUGSRV() //if(smtpServer.canRead(1)) cout << "s:" << smtpServer.readLine() << endl;
GET_RESPONSE_AND_CHECK("220","CONNECT");
SEND_COMMAND_AND_CHECK("HELO localhost","250","EHLO");
SEND_COMMAND_AND_CHECK("MAIL FROM: \"hermes daemon\"<hermes@localhost>","250","MAIL FROM");
SEND_COMMAND_AND_CHECK("RCPT TO: "+address,"250","RCPT TO");
SEND_COMMAND_AND_CHECK("DATA","354","DATA");
smtpServer.writeLine("From: \"hermes daemon\"<hermes@localhost>");
DEBUGSRV();
smtpServer.writeLine("To: "+address);
DEBUGSRV();
smtpServer.writeLine("Subject: Exception happened on hermes");
DEBUGSRV();
smtpServer.writeLine("");
DEBUGSRV();
smtpServer.writeLine("Hello "+address+",");
DEBUGSRV();
smtpServer.writeLine("Hermes gave an error, with the following error message:");
DEBUGSRV();
smtpServer.writeLine(message);
DEBUGSRV();
smtpServer.writeLine(".");
GET_RESPONSE_AND_CHECK("250","sending data contents");
SEND_COMMAND_AND_CHECK("QUIT","221","QUIT");
#undef ERROR_SENDING
#undef GET_RESPONSE_AND_CHECK
#undef SEND_COMMAND_AND_CHECK
}
#else //THEN USE sendmail to send the email
void Exception::notifyByEmail(string message)
{
FILE *sendmail;
string address=cfg.getNotifyTo();
sendmail=popen("/var/qmail/bin/sendmail -t","w");
if(NULL==sendmail)
throw NotifyException(_("Couldn't initialize sendmail command"));
fprintf(sendmail,"From: \"hermes daemon\"<hermes@localhost>\n");
fprintf(sendmail,"To: %s\n",address.c_str());
fprintf(sendmail,"Subject: Exception happened on hermes\n");
fprintf(sendmail,"\n");
fprintf(sendmail,"Hello %s,\n",address.c_str());
fprintf(sendmail,"Hermes gave an error, with the following error message:\n");
fprintf(sendmail,"%s\n",message.c_str());
fflush(sendmail);
pclose(sendmail);
}
#endif //USE_SMTP_FOR_NOTIFICATIONS
#endif //NOTIFY_EXCEPTIONS

64
src/Exception.h Normal file
View file

@ -0,0 +1,64 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#ifndef EXCEPTION_H
#define EXCEPTION_H
#include <iostream>
#include <string>
#include "Configfile.h"
#include "Logger.h"
using namespace std;
class Exception
{
private:
string error;
string file;
unsigned line;
void notifyByEmail(string);
public:
Exception(string,string,unsigned);
operator string();
friend ostream& operator<<(ostream&,Exception&);
};
class NetworkException:public Exception
{
public:
NetworkException(string p_error,string p_file,int p_line):Exception(p_error,p_file,p_line){}
};
class SQLException:public Exception
{
public:
SQLException(string p_error,string p_file,int p_line):Exception(p_error,p_file,p_line){}
};
class NotifyException
{
private:
string error;
public:
NotifyException(string p_error){ error=p_error; }
operator string(){ return "NotifyException: "+error;};
};
#endif //EXCEPTION_H

156
src/FileLogger.cpp Normal file
View file

@ -0,0 +1,156 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#include "FileLogger.h"
extern Configfile cfg;
FileLogger::FileLogger():linecount(0)
{
pthread_mutex_init(&mutex,NULL);
tmpstrings.clear();
last_rotation=time(NULL);
}
FileLogger::~FileLogger()
{
if(!cfg.getKeepFileLocked())
syncBufferToDisk();
closeFile();
pthread_mutex_destroy(&mutex);
}
void FileLogger::openFile(string file)
{
if(NULL==f&&""!=file)
{
f=fopen(file.c_str(),"a");
if(NULL==f)
throw Exception(_("Couldn't start file logger, couldn't open ")+cfg.getFileLoggerFilename(),__FILE__,__LINE__);
}
}
void FileLogger::closeFile()
{
if(NULL!=f)
{
fclose(f);
f=NULL;
}
}
void FileLogger::syncBufferToDisk()
{
openFile(cfg.getFileLoggerFilename());
if(NULL!=f)
{
for(list<string>::iterator i=tmpstrings.begin();i!=tmpstrings.end();i++)
fprintf(f,"%s\n",i->c_str());
closeFile();
tmpstrings.clear();
}
}
void FileLogger::addMessage(int loglevel,string logmessage)
{
pthread_mutex_lock(&mutex);
if(cfg.getLogRotationFrequency()>0&&last_rotation+(cfg.getLogRotationFrequency()*60)<time(NULL))
{
#ifdef REALLY_VERBOSE_DEBUG
cout << "Rotating log to file " << getProcessedRotateFilename() << " at " << time(NULL) << " with a last rotation of " << last_rotation << endl;
#endif //REALLY_VERBOSE_DEBUG
rotateLog();
}
try
{
if(!cfg.getKeepFileLocked())
tmpstrings.push_back(Utils::rfc2821_date()+": "+logmessage);
else
{
openFile(cfg.getFileLoggerFilename());
if(NULL!=f)
fprintf(f,"%s: %s\n",Utils::rfc2821_date().c_str(),logmessage.c_str());
}
if(++linecount>30)
{
linecount=0;
if(!cfg.getKeepFileLocked())
syncBufferToDisk();
else if(NULL!=f)
fflush(f);
}
}
catch(Exception &e)
{
pthread_mutex_unlock(&mutex);
throw e;
}
pthread_mutex_unlock(&mutex);
}
void FileLogger::rotateLog()
{
string filename="";
if(!cfg.getKeepFileLocked())
syncBufferToDisk();
closeFile();
filename=getProcessedRotateFilename();
if(-1==rename(cfg.getFileLoggerFilename().c_str(),filename.c_str()))
throw Exception("Error renaming logfile to "+filename+": "+Utils::errnotostrerror(errno),__FILE__,__LINE__);
last_rotation=time(NULL);
}
#define SUBSTITUTE(x) \
sizetemp=tmpstr.find("%%" #x "%%");\
if(sizetemp!=string::npos)\
tmpstr.replace(sizetemp,strlen("%%" #x "%%"),x);
#define GETDATECHR(x,y) \
strftime(tmpchar,sizeof(tmpchar),x,local_time);\
y=tmpchar;
string FileLogger::getProcessedRotateFilename()
{
string tmpstr=cfg.getRotateFilename();
string::size_type sizetemp;
string year,month,day,hour,minute;
char tmpchar[5];
struct tm *local_time;
time_t t;
t=time(NULL);
local_time=localtime(&t);
GETDATECHR("%Y",year);
GETDATECHR("%m",month);
GETDATECHR("%d",day);
GETDATECHR("%H",hour);
GETDATECHR("%M",minute);
SUBSTITUTE(day);
SUBSTITUTE(month);
SUBSTITUTE(year);
SUBSTITUTE(hour);
SUBSTITUTE(minute);
return tmpstr;
}

58
src/FileLogger.h Normal file
View file

@ -0,0 +1,58 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#ifndef FILELOGGER_H
#define FILELOGGER_H
#include "config.h"
#include <stdio.h>
#include <pthread.h>
#include <string>
#include <list>
#include "Logger.h"
#include "Configfile.h"
using namespace std;
/**
* this class implements a logger that writes to a file
*
* @see Logger
*/
class FileLogger: public Logger
{
unsigned char linecount;
time_t last_rotation;
private:
FILE *f;
pthread_mutex_t mutex;
list<string> tmpstrings;
void openFile(string);
void closeFile();
void syncBufferToDisk();
void rotateLog();
string getProcessedRotateFilename();
public:
FileLogger();
~FileLogger();
void addMessage(int,string);
};
#endif //FILELOGGER_H

49
src/Logger.h Normal file
View file

@ -0,0 +1,49 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#ifndef LOGGER_H
#define LOGGER_H
#include <string>
using namespace std;
/**
* logger base class
*/
class Logger
{
public:
virtual ~Logger(){}; //empty destructor, not creating anything
virtual void addMessage(int,string)=0;
};
#ifndef WIN32
#include "UnixLogger.h"
#endif //WIN32
#ifndef LOG_INFO
#define LOG_INFO 0
#define LOG_DEBUG 1
#define LOG_ERR 2
#endif //LOG_INFO
#include "FileLogger.h"
#include "NullLogger.h"
#endif //LOGGER_H

33
src/Makefile.am Normal file
View file

@ -0,0 +1,33 @@
INCLUDES = $(OpenSSL_CFLAGS) $(SQLite3_CFLAGS)
LIBS = $(OpenSSL_LIBS) $(SQLite3_LIBS)
CXXFLAGS += -Wall -ansi -pedantic -Wshadow -pthread
bin_PROGRAMS = hermes
nodist_hermes_SOURCES = Configfile.cpp
hermes_SOURCES = Proxy.cpp ServerSocket.cpp Socket.cpp Database.cpp Utils.cpp Exception.cpp hermes.cpp
noinst_HEADERS = Proxy.h ServerSocket.h Socket.h Database.h UnixLogger.cpp FileLogger.h NullLogger.h Logger.h Utils.h Exception.h hermes.h Spf.h
EXTRA_DIST = Configfile.cpp.in Configfile.h.in Configfile.tmpl UnixLogger.cpp UnixLogger.h FileLogger.cpp FileLogger.h
if LOGGER_UNIX
hermes_SOURCES += UnixLogger.cpp
endif
if LOGGER_FILE
hermes_SOURCES += FileLogger.cpp
endif
if HAVE_SPF
hermes_SOURCES += Spf.cpp
LIBS += -lspf2
endif
if WIN32_SERVICE
hermes_SOURCES += win32-service.cpp
endif
Configfile.h: Configfile.cpp.in Configfile.h.in Configfile.tmpl ../docs/hermes-options.html.in ../scripts/generate_config.pl ../config.h
cpp Configfile.tmpl|../scripts/generate_config.pl
Configfile.cpp: Configfile.h Configfile.cpp.in Configfile.h.in Configfile.tmpl ../docs/hermes-options.html.in ../scripts/generate_config.pl ../config.h
*.cpp: Configfile.h

41
src/NullLogger.h Normal file
View file

@ -0,0 +1,41 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#ifndef NULLLOGGER_H
#define NULLLOGGER_H
#include <string>
#include "Logger.h"
using namespace std;
/**
* this logger implements a null logger
* messages added to this logger go nowhere
*
* @see Logger
*/
class NullLogger: public Logger
{
public:
void addMessage(int,string){}; //ignore messages
};
#endif //NULLLOGGER_H

334
src/Proxy.cpp Normal file
View file

@ -0,0 +1,334 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#include "Proxy.h"
extern LOGGER_CLASS hermes_log;
extern Configfile cfg;
void Proxy::setOutside(Socket& p_outside)
{
outside=p_outside;
}
/**
* this function is the main part of the program, it just sniffs traffic
* between server and client and acts acording to the following diagram:
*
* TODO: fill diagram and point to website with graphical version
*
*/
void Proxy::run(string &peer_address)
{
#ifdef REALLY_VERBOSE_DEBUG
string debugLog="\r\n";
#endif //REALLY_VERBOSE_DEBUG
#ifdef HAVE_SPF
Spf spf_checker;
#endif //HAVE_SPF
string from="";
string to="";
string ehlostr="";
string resolvedname="";
unsigned char last_state=SMTP_STATE_WAIT_FOR_HELO;
try
{
bool throttled=cfg.getThrottle(); //we start with a throttled connection
bool authenticated=false; //we start with a non-authenticated connection
bool esmtp=false;
string strtemp;
//check whitelist
if(!cfg.getDnsWhitelistDomains().empty()&&Utils::listed_on_dns_lists(cfg.getDnsWhitelistDomains(),cfg.getDnsWhitelistPercentage(),peer_address))
{
authenticated=true;
if(true==cfg.getWhitelistedDisablesEverything())
throttled=false;
}
if(true==cfg.getWhitelistedDisablesEverything()&&Utils::whitelisted(cfg.getDatabaseFile(),peer_address))
{
throttled=false;
authenticated=true;
}
else
{
if(false==cfg.getAllowDataBeforeBanner())
{
sleep(cfg.getBannerDelayTime());
if(outside.canRead(0)) //if we have data waiting before the server gives us a 220 then quit, it's spam
{
#ifdef REALLY_VERBOSE_DEBUG
cout << peer_address << " sent data before 220, exiting..." << endl;
#endif //REALLY_VERBOSE_DEBUG
hermes_log.addMessage(LOG_INFO,"421 (data_before_banner) (ip:"+peer_address+")");
sleep(20); // but first let's annoy spammers once more
outside.writeLine("421 Stop sending data before we show you the banner");
return;
}
}
}
inside.init();
inside.connect(cfg.getServerHost(),cfg.getServerPort());
#ifdef HAVE_SSL
if(cfg.getOutgoingSsl())
inside.enableSSL(false);
if(cfg.getIncomingSsl())
outside.enableSSL(true);
#endif //HAVE_SSL
while(!outside.isClosed()&&!inside.isClosed())
{
if(outside.canRead(0.2)) //client wants to send something to server
{
strtemp=outside.readLine();
#ifdef REALLY_VERBOSE_DEBUG
debugLog+="c> "+strtemp+"\r\n";
#endif //REALLY_VERBOSE_DEBUG
if(outside.isClosed())
return;
if(strtemp.length()>10&&"mail from:"==Utils::strtolower(strtemp.substr(0,10)))
{
from=Utils::getmail(strtemp);
last_state=SMTP_STATE_WAIT_FOR_RCPTTO;
}
if("ehlo"==Utils::strtolower(strtemp.substr(0,4)))
esmtp=true;
if(strtemp.length()>4&&("ehlo"==Utils::strtolower(strtemp.substr(0,4))||"helo"==Utils::strtolower(strtemp.substr(0,4))))
{
ehlostr=Utils::trim(strtemp.substr(5));
last_state=SMTP_STATE_WAIT_FOR_MAILFROM;
}
if(strtemp.length()>8&&"rcpt to:"==Utils::strtolower(strtemp.substr(0,8)))
{
string strlog="";
string code="";
string mechanism="";
string message="";
to=Utils::getmail(strtemp);
try
{
resolvedname=Socket::resolveInverselyToString(peer_address);
}
catch(Exception &e)
{
resolvedname="";
}
strlog="from "+from+" (ip:"+peer_address+", hostname:"+resolvedname+", "+(esmtp?"ehlo":"helo")+":"+ehlostr+") -> to "+to;
//check greylisting
if(cfg.getGreylist()&&!authenticated&&Utils::greylist(cfg.getDatabaseFile(),peer_address,from,to))
{
//should we greylist¿? if we have to, quit and then sleep 20 seconds before closing the connection
code="421";
mechanism="greylist";
message=code+" Greylisted!! Please try again in a few minutes.";
#ifdef REALLY_VERBOSE_DEBUG
cout << "Checking " << mechanism << endl;
#endif //REALLY_VERBOSE_DEBUG
}
#ifdef HAVE_SPF
else if(cfg.getQuerySpf()&&!authenticated&&!spf_checker.query(peer_address,ehlostr,from))
{
code=cfg.getReturnTempErrorOnReject()?"421":"550";
mechanism="spf";
message=code+" You do not seem to be allowed to send email for that particular domain.";
#ifdef REALLY_VERBOSE_DEBUG
cout << "Checking " << mechanism << endl;
#endif //REALLY_VERBOSE_DEBUG
}
#endif //HAVE_SPF
//check blacklist
else if(!authenticated&&Utils::blacklisted(cfg.getDatabaseFile(),peer_address,to))
{
code=cfg.getReturnTempErrorOnReject()?"421":"550";
mechanism="allowed-domain-per-ip";
message=code+" You do not seem to be allowed to send email to that particular domain from that address.";
#ifdef REALLY_VERBOSE_DEBUG
cout << "Checking " << mechanism << endl;
#endif //REALLY_VERBOSE_DEBUG
}
//check rbl
else if(!cfg.getDnsBlacklistDomains().empty()&&!authenticated&&Utils::listed_on_dns_lists(cfg.getDnsBlacklistDomains(),cfg.getDnsBlacklistPercentage(),peer_address))
{
code=cfg.getReturnTempErrorOnReject()?"421":"550";
mechanism="dnsbl";
message=code+" You are listed on some DNS blacklists. Get delisted before trying to send us email.";
#ifdef REALLY_VERBOSE_DEBUG
cout << "Checking " << mechanism << endl;
#endif //REALLY_VERBOSE_DEBUG
}
else if(cfg.getRejectNoReverseResolution()&&!authenticated&&""==resolvedname)
{
code=cfg.getReturnTempErrorOnReject()?"421":"550";
mechanism="no reverse resolution";
message=code+" Your IP address does not resolve to a hostname.";
#ifdef REALLY_VERBOSE_DEBUG
cout << "Checking " << mechanism << endl;
#endif //REALLY_VERBOSE_DEBUG
}
else if(cfg.getCheckHeloAgainstReverse()&&!authenticated&&ehlostr!=resolvedname)
{
code=cfg.getReturnTempErrorOnReject()?"421":"550";
mechanism="helo differs from resolved name";
message=code+" Your IP hostname doesn't match your envelope hostname.";
#ifdef REALLY_VERBOSE_DEBUG
cout << "Checking " << mechanism << endl;
#endif //REALLY_VERBOSE_DEBUG
}
else
code="250";
if(""!=mechanism)
strlog.insert(0,"("+mechanism+") ");
strlog.insert(0,code+" ");
//log the connection
#ifdef REALLY_VERBOSE_DEBUG
cout << strlog << endl;
#endif //REALLY_VERBOSE_DEBUG
hermes_log.addMessage(LOG_INFO,strlog);
//if we didn't accept the email, punish spammers
if("250"!=code)
{
inside.writeLine("QUIT");
inside.close(); //close the socket now and leave server alone
sleep(20);
outside.writeLine(message);
return;
}
last_state=SMTP_STATE_WAIT_FOR_DATA;
}
if("starttls"==Utils::strtolower(strtemp.substr(0,8)))
{
//if we have ssl then accept starttls, if not politely say fuck you
#ifdef HAVE_SSL
try
{
#ifdef REALLY_VERBOSE_DEBUG
cout << peer_address << " enabled starttls, using secure comunications!!!" << endl;
#endif //REALLY_VERBOSE_DEBUG
outside.writeLine("220 You can speak now, line is secure!!");
outside.enableSSL(true);
}
catch(Exception &e)
{
#ifdef REALLY_VERBOSE_DEBUG
cout << string(e) << endl;
#endif //REALLY_VERBOSE_DEBUG
hermes_log.addMessage(LOG_DEBUG,string(e));
}
#else
outside.writeLine("454 TLS temporarily not available");
#endif //HAVE_SSL
strtemp="";
}
if(strtemp.length())
inside.writeLine(strtemp);
}
if(inside.canRead(0.2)) //server wants to send something to client
{
strtemp=inside.readLine();
#ifdef REALLY_VERBOSE_DEBUG
debugLog+="s> "+strtemp+"\r\n";
#endif //REALLY_VERBOSE_DEBUG
if(inside.isClosed())
return;
string code=strtemp.substr(0,3); //all responses by the server start with a code
if("354"==code) //354 -> you can start sending data, unthrottle now and read binary-safe
{
string endofdata="";
ssize_t bytes_read=0;
char buffer[4097];
outside.writeLine(strtemp);
strtemp="";
if(cfg.getAddHeaders())
{
inside.writeLine("Received: from "+ehlostr+" ("+peer_address+")");
inside.writeLine(" by "+Utils::gethostname()+" with "+(esmtp?"ESTMP":"SMTP")+" via TCP; "+Utils::rfc2821_date());
inside.writeLine("X-Anti-Spam-Proxy: Proxied by Hermes [www.hermes-project.com]");
}
do
{
bytes_read=outside.readBytes(buffer,sizeof(buffer)-1);
if(bytes_read<1)
throw NetworkException("Problem reading DATA contents, recv returned "+Utils::inttostr(bytes_read),__FILE__,__LINE__);
buffer[bytes_read]='\0';
inside.writeBytes(buffer,bytes_read);
if(bytes_read<5)
endofdata+=string(buffer);
else
endofdata=string(buffer+bytes_read-5);
if(endofdata.length()>5)
endofdata=endofdata.substr(endofdata.length()-5);
}
while(endofdata!="\r\n.\r\n"/*&&endofdata.length()>3&&endofdata.substr(2)!="\n.\n"&&endofdata.substr(2)!="\r.\r"*/);
}
if("235"==code) //235 -> you are correctly authenticated, unthrottle & authenticate
{
throttled=false;
authenticated=true;
}
if("250-pipelining"==Utils::strtolower(strtemp)) //this solves our problems with pipelining-enabled servers
strtemp="";
//this is a special case, we can't just ignore the line if it's the last line (doesn't have the dash after the code)
//so we just say we support an imaginary extension (noextension).
//caveat: this makes us identificable, so, if you can, configure your smtp server to either don't support pipelining
//or to not advertise it as the last capability.
if("250 pipelining"==Utils::strtolower(strtemp))
strtemp="250 x-noextension";
if(strtemp.length())
outside.writeLine(strtemp);
}
if(throttled)
sleep(cfg.getThrottlingTime()); //we take 1 second between each command to make spammers angry
}
}
catch(Exception &e) //any exception will close both connections
{
#ifdef REALLY_VERBOSE_DEBUG
cout << e << endl;
try
{
throw Exception(string(e)+debugLog+"\r\n",__FILE__,__LINE__);
}
catch(Exception &ex){}
#endif //REALLY_VERBOSE_DEBUG
if(last_state<SMTP_STATE_WAIT_FOR_DATA)
hermes_log.addMessage(LOG_INFO,"421 (throttling) from "+(""==from?"no-from":from)+" (ip:"+peer_address+", hostname:"+(""==resolvedname?"not-resolved":resolvedname)+", ehlo:"+(""==ehlostr?"no-ehlo":ehlostr)+") -> to "+(""==to?"no-to":to));
return;
}
}

61
src/Proxy.h Normal file
View file

@ -0,0 +1,61 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#ifndef PROXY_H
#define PROXY_H
#include "hermes.h"
#include <sys/param.h>
#ifdef WIN32
#include <winsock2.h>
#else
#include <sys/select.h>
#endif
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
#include "Socket.h"
#include "Configfile.h"
#include "Utils.h"
#include "Logger.h"
#ifdef HAVE_SPF
#include "Spf.h"
#endif //HAVE_SPF
#define SMTP_STATE_WAIT_FOR_HELO 0
#define SMTP_STATE_WAIT_FOR_MAILFROM 1
#define SMTP_STATE_WAIT_FOR_RCPTTO 2
#define SMTP_STATE_WAIT_FOR_DATA 3
class Proxy
{
private:
Socket outside; //connection from someone sending mail
Socket inside; //connection to our inside smtp
public:
//Proxy():outside(NULL),inside(NULL){};
void setOutside(Socket&);
void run(string&);
};
#endif //PROXY_H

88
src/ServerSocket.cpp Normal file
View file

@ -0,0 +1,88 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#include "ServerSocket.h"
void ServerSocket::setPort(unsigned int p_port)
{
port=p_port;
}
void ServerSocket::listen()
{
struct sockaddr_in address;
address.sin_family=AF_INET;
#ifndef WIN32
if("any"==listen_ip||""==listen_ip)
address.sin_addr.s_addr=INADDR_ANY;
else
if(!inet_aton(listen_ip.c_str(),&address.sin_addr))
throw Exception(_("IP address ")+listen_ip+_(" is not valid"),__FILE__,__LINE__);
#else
address.sin_addr.s_addr=INADDR_ANY;
#endif //WIN32
address.sin_port=htons(port);
// ...and allow reuse of the socket
int i=1;
setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,(char *)&i,sizeof(i));
if(bind(fd,(sockaddr*)&address,sizeof(address))==-1)
{
close();
throw Exception(_("Error: binding to address ")+(""==listen_ip?"any":listen_ip)+":"+Utils::inttostr(port),__FILE__,__LINE__);
}
if(::listen(fd,10)==-1)
throw Exception(_("Error: listening"),__FILE__,__LINE__);
}
/**
* convenience wrapper for listen
*
* @param port port to listen at
* @param ip ip to bind to
*/
void ServerSocket::listen(unsigned int p_port,string ip)
{
setPort(p_port);
setListenIP(ip);
listen();
}
void ServerSocket::setListenIP(string& ip)
{
listen_ip=ip;
}
int ServerSocket::accept(string *straddr)
{
struct sockaddr_in address;
socklen_t addresslength=sizeof(address);
int retval;
retval=::accept(fd,(sockaddr *)&address,&addresslength);
if(-1==retval)
throw Exception(_(Utils::inttostr(retval)),__FILE__,__LINE__);
if(straddr!=NULL)
(*straddr)=inet_ntoa(address.sin_addr);
return retval;
}

57
src/ServerSocket.h Normal file
View file

@ -0,0 +1,57 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#ifndef SERVERSOCKET_H
#define SERVERSOCKET_H
#include "config.h"
#include <sys/types.h>
#ifdef WIN32
#include <winsock2.h>
#else
#include <sys/socket.h>
#include <netdb.h>
#include <libintl.h>
#endif //WIN32
#include <iostream>
#include "hermes.h"
#include "Socket.h"
/**
* implement the server specific methods for socket, mainly listen() and accept()
* @see Socket
*/
class ServerSocket: public Socket
{
private:
unsigned int port;
string listen_ip;
public:
ServerSocket(){};
~ServerSocket(){};
void setPort(unsigned int);
void setListenIP(string&);
void listen();
void listen(unsigned int,string);
int accept(string *);
};
#endif //SERVERSOCKET_H

577
src/Socket.cpp Normal file
View file

@ -0,0 +1,577 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#include "Socket.h"
int Socket::created_sockets=0;
#ifdef HAVE_SSL
SSL_CTX *Socket::ssl_ctx_server=NULL;
SSL_CTX *Socket::ssl_ctx_client=NULL;
#endif //HAVE_SSL
extern Configfile cfg;
Socket::Socket():fd(-1)
#ifdef HAVE_SSL
,ssl_enabled(false),ssl(NULL)
#endif //HAVE_SSL
{
if(!created_sockets)
{
#ifdef WIN32
/* fuck windows, it needs this sh*t before allowing sockets to work */
WSADATA wsaData;
int wsa=WSAStartup(0xff,&wsaData);
if(wsa)
{
perror("Windows not working, call microsoft");
exit(-1);
}
#endif //WIN32
#ifdef HAVE_SSL
SSL_library_init();
SSL_load_error_strings();
//initialize the context for both server and client operation
//server
ssl_ctx_server=NULL;
ssl_ctx_server=SSL_CTX_new(SSLv23_server_method());
/* create context */
if(!ssl_ctx_server)
throw Exception(_("Error creating SSL context"),__FILE__,__LINE__);
/* load certificate */
if(SSL_CTX_use_certificate_file(ssl_ctx_server,cfg.getCertificateFile().c_str(),SSL_FILETYPE_PEM)==-1)
throw Exception(_("Error loading certificate"),__FILE__,__LINE__);
/* load private key */
if(SSL_CTX_use_PrivateKey_file(ssl_ctx_server,cfg.getPrivateKeyFile().c_str(),SSL_FILETYPE_PEM)==-1)
throw Exception(_("Error loading private key"),__FILE__,__LINE__);
/* check that private key and cert match */
if(!SSL_CTX_check_private_key(ssl_ctx_server))
throw Exception(_("Private key doesn't match certificate file"),__FILE__,__LINE__);
//client
ssl_ctx_client=NULL;
ssl_ctx_client=SSL_CTX_new(SSLv23_client_method());
if(!ssl_ctx_client)
throw Exception(_("Error creating SSL context"),__FILE__,__LINE__);
//set options to make SSL_read and SSL_write behave more like read and write
SSL_CTX_set_mode(ssl_ctx_server,SSL_MODE_ENABLE_PARTIAL_WRITE); //PARTIAL_WRITE allows a write to suceed with fewer bytes sent
SSL_CTX_set_mode(ssl_ctx_client,SSL_MODE_ENABLE_PARTIAL_WRITE);
SSL_CTX_set_mode(ssl_ctx_server,SSL_MODE_AUTO_RETRY); //AUTO_RETRY will block SSL_read and SSL_write if a renegotiation is required
SSL_CTX_set_mode(ssl_ctx_client,SSL_MODE_AUTO_RETRY);
#endif //HAVE_SSL
}
created_sockets++;
}
/**
*
* we have constructor and init because we may want to create the object
* and afterwards assign an fd obtained in other way with setFD
*
*/
void Socket::init()
{
// create socket ...
fd=socket(PF_INET,SOCK_STREAM,0);
//configure timeout
setTimeout(60,60);
if(fd==-1)
throw Exception(_(string("Error creating socket :")+Utils::inttostr(errno)+" "+Utils::errnotostrerror(errno)),__FILE__,__LINE__);
}
/**
* close the socket
*/
void Socket::close()
{
if(fd!=-1)
{
/* shutdown(fd,SHUT_RDWR); */ //we don't care about return, if we are on eof we are already closed and this doesn't hurt
//the same applies to close/closesocket
#ifndef WIN32
::close(fd);
#else
closesocket(fd);
#endif //WIN32
fd=-1;
}
}
/**
* destructor.
*
* we close the socket if it's still open, and we
* decrement the created_sockets count
* when the count is 0, destroy the ssl contexts and,
* if on windows, do a WSACleanup
*/
Socket::~Socket()
{
#ifdef HAVE_SSL
if(ssl_enabled&&ssl!=NULL)
SSL_free(ssl);
#endif //HAVE_SSL
close();
created_sockets--;
if(!created_sockets)
{
#ifdef WIN32
WSACleanup();
#endif //WIN32
#ifdef HAVE_SSL
if(ssl_enabled&&ssl_ctx_server!=NULL)
SSL_CTX_free(ssl_ctx_server);
if(ssl_enabled&&ssl_ctx_client!=NULL)
SSL_CTX_free(ssl_ctx_client);
#endif //HAVE_SSL
}
}
#ifdef HAVE_SSL
/**
* enable ssl on the socket
*
* @param server whether to enable server ssl or client ssl
*/
void Socket::enableSSL(bool server)
{
if(server)
ssl=SSL_new(ssl_ctx_server);
else
ssl=SSL_new(ssl_ctx_client);
if(ssl!=NULL)
{
SSL_set_fd(ssl,fd);
ssl_enabled=true;
}
else
throw Exception(_("Error creating ssl structure"),__FILE__,__LINE__);
if(server)
SSL_accept(ssl);
else
SSL_connect(ssl);
}
#endif //HAVE_SSL
bool Socket::isClosed()
{
return -1==fd;
}
/**
* read lon bytes from the socket to buf buffer
*
* @param buf buffer to fill(must be previously reserved)
* @param lon number of bytes to read
* @return number of bytes read
*/
ssize_t Socket::readBytes(void *buf,ssize_t lon)
{
ssize_t retval=0; //shut compiler up
ssize_t readed=0;
while(readed<lon)
{
if(-1==fd)
throw Exception(_("Trying to read a non-opened or already closed socket"),__FILE__,__LINE__);
#ifdef HAVE_SSL
if(ssl_enabled)
retval=SSL_read(ssl,buf,lon);
else
#endif //HAVE_SSL
retval=recv(fd,(char *)buf,lon,MSG_NOSIGNAL);
if(!retval)
throw NetworkException(_("Peer closed connection"),__FILE__,__LINE__);
if(retval<0)
#ifdef HAVE_SSL
if(ssl_enabled)
throw NetworkException(_("SSL error number: ")+Utils::inttostr(SSL_get_error(ssl,retval)),__FILE__,__LINE__);
else
#endif //HAVE_SSL
throw NetworkException(_(Utils::errnotostrerror(errno)),__FILE__,__LINE__);
readed+=lon;
}
return retval;
}
/**
* read a single byte from the socket
*
* @return the read byte(char)
*/
char Socket::readByte()
{
char c=0;
readBytes(&c,1);
return c;
}
/**
* read a full line and return a string with it
*
* @return the read string
*/
string Socket::readLine()
{
char c=0;
stringstream s;
do
{
c=readByte();
if(c!=10&&c!=13&&c!=0)
s<<c;
}
while(c!=10&&!isClosed());
return s.str();
}
void Socket::writeBytes(void *bytes,ssize_t len)
{
int retval;
ssize_t written=0;
if(fd==-1)
throw Exception(_("Trying to write to a non-opened socket"),__FILE__,__LINE__);
while(written<len)
{
#ifdef HAVE_SSL
if(ssl_enabled)
retval=SSL_write(ssl,bytes,len);
else
#endif //HAVE_SSL
retval=send(fd,(char *)bytes,len,MSG_NOSIGNAL);
if(!retval)
throw NetworkException(_("Peer closed connection"),__FILE__,__LINE__);
if(retval<0)
#ifdef HAVE_SSL
if(ssl_enabled)
throw NetworkException(_("SSL error number: ")+Utils::inttostr(SSL_get_error(ssl,retval)),__FILE__,__LINE__);
else
#endif //HAVE_SSL
throw NetworkException(_(Utils::errnotostrerror(errno)),__FILE__,__LINE__);
written+=len;
}
}
void Socket::writeByte(char c)
{
writeBytes(&c,sizeof(char));
}
void Socket::writeLine(string s)
{
s+="\r\n";
writeBytes((void *)s.c_str(),s.length());
}
void Socket::setFD(int p_fd)
{
if(fd>0)
close();
if(p_fd>0)
{
fd=p_fd;
setTimeout(60,60);
}
else
throw Exception(_("Error: fd not valid"),__FILE__,__LINE__);
}
/**
* returns true if there's data waiting to be read on the socket
* if it's a serversocket means that a new connection is waiting
*
* @param unsigned int seconds to wait before returning true or false
*
* @return bool true if there's data(or a connection waiting) and false if there's not
*
*/
bool Socket::canRead(float time)
{
fd_set rfd;
struct timeval timeout;
int seconds=0;
int useconds=0;
//calculate seconds and useconds
seconds=int(time);
useconds=int((time-seconds)*1000);
//set rfd to the fd of our connection
FD_ZERO(&rfd);
FD_SET(fd,&rfd);
//we wait x seconds for an incoming connection
timeout.tv_usec=useconds;
timeout.tv_sec=seconds;
if(select(fd+1,&rfd,NULL,NULL,&timeout)>0)
return true;
else
#ifdef HAVE_SSL
if(ssl_enabled)
if(SSL_pending(ssl))
return true;
else
return false;
else
return false;
#else
return false;
#endif //HAVE_SSL
}
bool Socket::connect(string host,unsigned int port)
{
struct sockaddr address;
struct sockaddr_in *inetaddress;
address=(sockaddr)Socket::resolve(host);
inetaddress=(sockaddr_in *)&address;
inetaddress->sin_port=htons(port);
int retval=::connect(fd,&address,sizeof(address));
if(retval==-1)
throw Exception(string(_("Error connecting to "))+host+":"+Utils::inttostr(port)+" "+string("(")+Socket::resolveToString(host)+string(")"),__FILE__,__LINE__);
if(!retval)
return true;
else
return false;
}
#ifdef HAVE_GETADDRINFO
struct sockaddr Socket::resolve(string host)
{
struct addrinfo *hostinfo=NULL;
struct addrinfo hints;
struct sockaddr resolvedip;
int error;
//configure hints to use IPv4 and IPv6, and resolve ips to name
memset(&hints,0,sizeof(hints));
hints.ai_flags=AI_ADDRCONFIG;
hints.ai_family=AF_UNSPEC;
hints.ai_socktype=SOCK_STREAM;
hints.ai_protocol=IPPROTO_TCP;
hints.ai_addrlen=0;
hints.ai_addr=0;
hints.ai_canonname=NULL;
error=getaddrinfo(host.c_str(),NULL,&hints,&hostinfo);
if(error)
#ifdef HAVE_GAI_STRERROR
throw Exception(gai_strerror(error),__FILE__,__LINE__);
#else
#ifdef WIN32
throw Exception("Winsock error "+Utils::inttostr(WSAGetLastError()),__FILE__,__LINE__);
#else
throw Exception("Socket error number "+Utils::inttostr(error),__FILE__,__LINE__);
#endif //WIN32
#endif //HAVE_GAI_STRERROR
resolvedip=*(hostinfo->ai_addr);
freeaddrinfo(hostinfo);
return resolvedip;
}
string Socket::resolveToString(string host)
{
struct sockaddr hostinfo;
struct sockaddr_in *inetip;
string strip;
hostinfo=Socket::resolve(host);
inetip=(sockaddr_in*)&hostinfo;
strip=string(inet_ntoa(inetip->sin_addr));
return strip;
}
string Socket::resolveInverselyToString(string ip)
{
int error;
char hostname[NI_MAXHOST];
struct sockaddr addr;
addr=resolve(ip);
error=getnameinfo(&addr,sizeof(struct sockaddr),hostname,NI_MAXHOST,NULL,0,NI_NAMEREQD);
if(error)
if(error==EAI_NONAME) //if the problem is that we didn't get a hostname, return empty string
hostname[0]='\0';
else
#ifdef HAVE_GAI_STRERROR
throw Exception(gai_strerror(error)+Utils::inttostr(error),__FILE__,__LINE__);
#else
#ifdef WIN32
throw Exception("Winsock error "+Utils::inttostr(WSAGetLastError()),__FILE__,__LINE__);
#else
throw Exception("Socket error number "+Utils::inttostr(error),__FILE__,__LINE__);
#endif //WIN32
#endif //HAVE_GAI_STRERROR
return string(hostname);
}
#else
/*
* WARNING!!! WARNING!!! WARNING!!!
* the following 3 functions are NOT thread-safe UNLESS used on a platform
* that is using thread-local storage (i.e. windows), so be VERY careful with
* them
*/
struct sockaddr Socket::resolve(string host)
{
struct sockaddr_in *addr_in;
struct sockaddr addr;
addr_in=(sockaddr_in *)&addr;
addr_in->sin_addr.s_addr=inet_addr(Socket::resolveToString(host).c_str());
addr_in->sin_family=AF_INET;
return addr;
}
string Socket::resolveToString(string host)
{
struct hostent *hostinfo;
struct in_addr addr;
hostinfo=gethostbyname(host.c_str());
if(NULL==hostinfo)
throw Exception("Error resolving "+host,__FILE__,__LINE__);
memcpy(&addr,hostinfo->h_addr,sizeof(addr));
return string(inet_ntoa(addr));
}
string Socket::resolveInverselyToString(string ip)
{
struct hostent *hostinfo;
unsigned long addr;
assert(ip.length()<16);
addr=inet_addr(ip.c_str());
hostinfo=gethostbyaddr((char *)&addr,4,AF_INET);
if(NULL==hostinfo)
throw Exception("Error resolving "+ip,__FILE__,__LINE__);
return string(hostinfo->h_name);
}
#endif //HAVE_GETADDRINFO
int Socket::getFD()
{
return fd;
}
/**
* this function sets timeouts for a socket on receive and send
* if either recv or send is -1, the timeout is not set, so for example
* if you want to set recv timeout but NOT send timeout, you could call
* this function like this:
* Socket::setTimeout(fd,3,-1); <-- set 3 seconds receive timeout and
* don't change send timeout
*
* setting timeout of either one to 0 disables the timeout, socket will
* block forever for data
*
* this function is needed because setting these timeouts is one of the
* less portable setsockopt functions, and lot's of operating systems
* do it differently
*
* @see setsockopt(2), socket(7) $LINUX_SRC/net/core/sock.c:{sock_setsockopt,sock_set_timeout}
*
* @todo check procedure on other operating systems like Solaris, *BSD and others
*
* @param recv timeout for receiving, a number in the format of 1.5 (one second and 500 milliseconds)
* @param send same as recv but for sending
*/
void Socket::setTimeout(float recv,float send)
{
if(fd<=0)
throw Exception("Socket invalid: "+Utils::inttostr(fd)+" ",__FILE__,__LINE__);
#ifdef WIN32
//set timeout for receiving
if(recv)
{
unsigned timeout=int(recv*1000);
if(-1==setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,(char *)&timeout,sizeof(timeout)))
throw Exception(Utils::errnotostrerror(errno),__FILE__,__LINE__);
}
//set timeout for sending
if(send)
{
unsigned timeout=int(send*1000);
if(-1==setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,(char *)&timeout,sizeof(timeout)))
throw Exception(Utils::errnotostrerror(errno),__FILE__,__LINE__);
}
#else
struct timeval timeout;
int seconds=0;
int useconds=0;
//calculate seconds and useconds
if(recv)
{
seconds=int(recv);
useconds=int((recv-seconds)*1000);
timeout.tv_usec=useconds;
timeout.tv_sec=seconds;
if(-1==setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,(char *)&timeout,sizeof(timeout)))
throw Exception(Utils::errnotostrerror(errno),__FILE__,__LINE__);
}
if(send)
{
seconds=int(send);
useconds=int((send-seconds)*1000);
timeout.tv_usec=useconds;
timeout.tv_sec=seconds;
if(-1==setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,(char *)&timeout,sizeof(timeout)))
throw Exception(Utils::errnotostrerror(errno),__FILE__,__LINE__);
}
#endif
}

103
src/Socket.h Normal file
View file

@ -0,0 +1,103 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#ifndef SOCKET_H
#define SOCKET_H
#include "hermes.h"
#include <iostream>
#include <string>
#include <sstream>
#include <sys/types.h>
#ifdef WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#ifndef AI_ADDRCONFIG
#define AI_ADDRCONFIG 0 //if the windows we are compiling at is old, define it as 0
#endif //AI_ADDRCONFIG
#else
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#endif //WIN32
#include <errno.h>
#ifdef HAVE_SSL
#include <openssl/ssl.h>
#endif //HAVE_SSL
//this is a bit of a hack
//if a system doesn't have MSG_NOSIGNAL then we define it as 0 (that is, no options selected)
//I've tried this on Solaris and NetBSD and it seems to work. Still, recheck in the future (TODO)
#ifndef MSG_NOSIGNAL
#define MSG_NOSIGNAL 0
#endif //MSG_NOSIGNAL
#include "Configfile.h"
#include "Utils.h"
using namespace std;
/**
* implements a socket class independent of operating system and that supports ssl
*/
class Socket
{
protected:
int fd;
private:
// bool closed;
static int created_sockets;
#ifdef HAVE_SSL
bool ssl_enabled;
SSL *ssl;
static SSL_CTX *ssl_ctx_client;
static SSL_CTX *ssl_ctx_server;
#endif //HAVE_SSL
public:
Socket();
~Socket();
#ifdef HAVE_SSL
void enableSSL(bool);
#endif //HAVE_SSL
void setFD(int);
bool canRead(float);
bool connect(string,unsigned int);
int getFD();
static struct sockaddr resolve(string);
static string resolveToString(string);
static string resolveInverselyToString(string);
void init();
void close();
//reading and writing
char readByte();
ssize_t readBytes(void *,ssize_t);
string readLine();
void writeByte(char);
void writeBytes(void *,ssize_t);
void writeLine(string);
bool isClosed();
void setTimeout(float,float);
};
#endif //SOCKET_H

99
src/Spf.cpp Normal file
View file

@ -0,0 +1,99 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#include "Spf.h"
SPF_server_t *Spf::spfserver=NULL;
/**
* constructor
*
* it will create a spfserver if this is the first created object of the class.
* if it isn't, then we just create an spfrequest
*/
Spf::Spf():spfrequest(NULL),spfresponse(NULL)
{
pthread_mutex_init(&mutex,NULL);
if(NULL==spfserver)
if(NULL==(spfserver=SPF_server_new(SPF_DNS_CACHE,0)))
throw Exception(_("Can't initialize SPF library"),__FILE__,__LINE__);
if(NULL==(spfrequest=SPF_request_new(spfserver)))
throw Exception(_("Can't initialize SPF request"),__FILE__,__LINE__);
}
/**
* destructor
*
* frees the memory of the spfrequest
*/
Spf::~Spf()
{
pthread_mutex_destroy(&mutex);
if(NULL!=spfrequest) SPF_request_free(spfrequest);
}
/**
* frees all memory related to the spf class
*
* this is needed because the common things are only initialized
* once (and are static), and when we close the program we need
* to deinitialize them
*/
void Spf::deinitialize()
{
if(NULL!=spfserver)
SPF_server_free(spfserver);
}
/**
* make a query to the dns system for an spf record
*
* highly inspired from fakehermes' source
*
* @param ip the ip of the remote server
* @param helo the hello string of the remote server
* @param from the envelope from address
*
* @returns true if it is not incorrect
*/
bool Spf::query(string ip,string helo,string from)
{
bool retval=false;
if(SPF_request_set_ipv4_str(spfrequest,ip.c_str()))
throw Exception(_("Error configuring IP for SPF request"),__FILE__,__LINE__);
if(SPF_request_set_helo_dom(spfrequest,helo.c_str()))
throw Exception(_("Error configuring HELO for SPF request"),__FILE__,__LINE__);
if(SPF_request_set_env_from(spfrequest,from.c_str()))
throw Exception(_("Error configuring FROM for SPF request"),__FILE__,__LINE__);
//make the actual query
pthread_mutex_lock(&mutex);
SPF_request_query_mailfrom(spfrequest,&spfresponse);
pthread_mutex_unlock(&mutex);
if(NULL!=spfresponse)
{
retval=(SPF_RESULT_FAIL==SPF_response_result(spfresponse)||SPF_RESULT_SOFTFAIL==SPF_response_result(spfresponse))?false:true;
SPF_response_free(spfresponse);
}
return retval;
}

44
src/Spf.h Normal file
View file

@ -0,0 +1,44 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#ifndef SPF_H
#define SPF_H
#include "hermes.h"
extern "C"
{
#include <spf2/spf.h>
}
class Spf
{
private:
static SPF_server_t *spfserver;
SPF_request_t *spfrequest;
SPF_response_t *spfresponse;
pthread_mutex_t mutex;
public:
Spf();
~Spf();
static void deinitialize();
bool query(string,string,string);
};
#endif //SPF_H

35
src/UnixLogger.cpp Normal file
View file

@ -0,0 +1,35 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#include "UnixLogger.h"
UnixLogger::UnixLogger()
{
openlog("hermes",LOG_NDELAY,LOG_MAIL);
}
UnixLogger::~UnixLogger()
{
closelog();
}
void UnixLogger::addMessage(int loglevel,string logmessage)
{
syslog(loglevel,logmessage.c_str());
}

43
src/UnixLogger.h Normal file
View file

@ -0,0 +1,43 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#ifndef UNIXLOGGER_H
#define UNIXLOGGER_H
#include <syslog.h>
#include <string>
#include "Logger.h"
using namespace std;
/**
* implements the logger for Linux/UNIX
*
* @see Logger
*/
class UnixLogger: public Logger
{
public:
UnixLogger();
~UnixLogger();
void addMessage(int,string);
};
#endif //UNIXLOGGER_H

580
src/Utils.cpp Normal file
View file

@ -0,0 +1,580 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#include "Utils.h"
extern Configfile cfg;
//--------------------
// string functions:
//--------------------
/**
* convert integer to string
*
* @param integer integer to convert
*
* @return string the integer converted to stringtype
*
*/
string Utils::inttostr(int integer)
{
stringstream s;
s << integer;
return s.str();
}
/**
* return lowercase version of s
*
* @param s string to convert
*
* @return string lowercase version of s
*
*/
string Utils::strtolower(string s)
{
for(unsigned int i=0;i<s.length();i++)
s[i]=tolower(s[i]);
return s;
}
/**
* trim spaces from both sides of the string
*
* @param s string to trim
*
* @return string trimmed string
*
*/
string Utils::trim(string s)
{
while(isspace(s[0]))
s.erase(0,1);
while(isspace(s[s.length()-1]))
s.erase(s.length()-1,1);
return s;
}
//------------------------
// email-related functions:
//------------------------
/**
* decide whether a triplet should be greylisted or not
*
* basically it follows this diagram:
*
* <pre>
* +------------------------------------------+
* | |yes
* | whitelisted?(IP or TO or DOMAIN or HOST) |----> don't greylist
* | |
* +------------------------------------------+
* |
* | no
* |
* v
* +----------------------------------+
* | |yes
* | blacklisted? (IP or FROM) |----> greylist
* | |
* +----------------------------------+
* |
* | no
* |
* v
* +----------------------------------+
* | |yes
* | greylisted? (triplet) |----> greylist
* | |
* +----------------------------------+
* |
* | no
* |
* v
* don't greylist
* </pre>
*
* @param dbfile sqlite database file to use. if doesn't exist then initialize
* @param ip ip of remote client, first of our triplet
* @param p_from mail from header, second of our triplet
* @param p_to rcpt to header, third of our triplet
*
* @return whether triplet should get greylisted or not
* @todo unify {white,black,grey}list in one function that returns a different constant in each case
*/
bool Utils::greylist(string dbfile,string& ip,string& p_from,string& p_to)
{
string from=Database::cleanString(p_from);
string to=Database::cleanString(p_to);
string hostname;
Database db;
db.setDatabaseFile(dbfile);
db.open();
try
{
hostname=Socket::resolveInverselyToString(ip);
}
catch(Exception &e)
{
hostname="unresolved";
}
if(db.whitelistedIP(ip)||db.whitelistedTO(to)||db.whitelistedDomain(getdomain(to))||(""!=hostname&&db.whitelistedHostname(hostname)))
return false;
if(db.blacklistedIP(ip)||db.blacklistedFROM(from)||db.blacklistedTO(to)||db.blacklistedToDomain(getdomain(to)))
return true;
if(db.greylisted(ip,from,to,cfg.getInitialExpiry(),cfg.getInitialBlacklist(),cfg.getWhitelistExpiry()))
return true;
else
return false;
}
/**
* whether an ip is listed on the database as whitelisted
*
* @param dbfile sqlite3 database file
* @param ip ip of remote machine
*
* @return whether ip is whitelisted or not
* @todo unify {white,black,grey}list in one function that returns a different constant in each case
*/
bool Utils::whitelisted(string dbfile,string& ip)
{
Database db;
string hostname;
db.setDatabaseFile(dbfile);
db.open();
try
{
hostname=Socket::resolveInverselyToString(ip);
}
catch(Exception &e)
{
hostname="unresolved";
}
if(db.whitelistedIP(ip)||(""!=hostname&&db.whitelistedHostname(hostname)))
return true;
return false;
}
/**
* whether an ip is listed on the database as blacklisted
*
* @param dbfile sqlite3 database file
* @param ip ip of remote machine
*
* @return whether ip is whitelisted or not
* @todo this should contain all cases when we should reject a connection
*/
bool Utils::blacklisted(string dbfile,string& ip,string& to)
{
Database db;
string hostname;
db.setDatabaseFile(dbfile);
db.open();
return false==db.allowedDomainPerIP(getdomain(to),ip);
}
/**
* this function extracts the email from rcpt to and mail from lines
* i.e.:
* rcpt to: "BillG" <bill@m.com> --> bill@m.com
*
* @param rawline line read from the socket
*
* @return email extracted from rawline
*
*/
string Utils::getmail(string& rawline)
{
string email;
string::size_type start=0,end=0;
start=rawline.find('@');
if(start!=string::npos) //case 1 -> email has an @(most common), start from there and look for spaces or </> as delimiters
{
start=rawline.find_last_of(":< ",start);
end=rawline.find_first_of("> ",start+1);
if(end==string::npos) //there is no space at the end, from start to end-of-line
end=rawline.length();
if(start!=string::npos&&end!=string::npos&&start<=end)
return trim(rawline.substr(start+1,end-start-1));
}
start=rawline.find(':');
start=rawline.find_first_of("< ",start);
if(start!=string::npos) // case 2 -> email is between <> or spaces and doesn't contain an @(i.e. local user)
{
start=rawline.find_first_not_of("< ",start+1);
if('"'==rawline[start]) // it can contain a name for the from(rcpt to: "BillG" billg@m.com)
{
start=rawline.find("\"",start+1);
start=rawline.find_first_not_of("< ",start+1);
}
end=rawline.find_first_of("> ",start+1);
if(start!=string::npos&&end!=string::npos&&start<=end)
return trim(rawline.substr(start,end-start));
}
return ""; //if there's a problem just return an empty string
}
/**
* this functions returns the domain part of email
* bill@m.com --> m.com
* it's mainly useful to whitelist destination domains, if
* for example a customer doesn't want any of it's emails using greylisting
* in contrast to only wanting _some_ specific email
*
* @param email email to get domain from
*
* @return domain of email
*
*/
string Utils::getdomain(string& email)
{
if(email.rfind('@'))
return trim(email.substr(email.rfind('@')+1));
else
return string("");
}
#ifndef WIN32
//-----------------
// posix-functions:
//-----------------
/**
* get uid from user
*
* keep in mind that this function is not thread-safe, because
* getpwnam isn't, and we don't actually care, this function
* gets called BEFORE we start spawning threads, but you should
* keep this details in mind if you intend to use this outside hermes
*
* @param user user to query
*
* @return uid for user
*
*/
int Utils::usertouid(string user)
{
struct passwd *pwd;
pwd=getpwnam(user.c_str());
if(NULL==pwd)
throw Exception(_("Error reading user data. user doesn't exist?"),__FILE__,__LINE__);
return pwd->pw_uid;
}
/**
* get gid from groupname
*
* IMPORTANT:
* keep in mind that this function is not thread-safe, because
* getgrnam isn't, and we don't actually care, this function
* gets called BEFORE we start spawning threads, but you should
* keep this details in mind if you intend to use this outside hermes
*
* @param groupname groupname to query
*
* @return gid for groupname
*
*/
int Utils::grouptogid(string groupname)
{
struct group *grp;
grp=getgrnam(groupname.c_str());
if(NULL==grp)
throw Exception(_("Error reading group data. group doesn't exist?"),__FILE__,__LINE__);
return grp->gr_gid;
}
#endif //WIN32
//----------------
// misc functions:
//----------------
/**
* whether a file is accesible by current process/user
*
* @param file file to check
*
* @return is file readable?
*
*/
bool Utils::file_exists(string file)
{
FILE *f=fopen(file.c_str(),"r");
if(NULL==f)
return false;
else
{
fclose(f);
return true;
}
}
/**
* whether a directory is accesible by current process/user
*
* @param string directory to check
*
* @return isdir readable?
*
*/
bool Utils::dir_exists(string dir)
{
DIR *d=opendir(dir.c_str());
if(NULL==d)
return false;
else
{
closedir(d);
return true;
}
}
/**
* return the error string corresponding to errnum
*
* @param errnum errno to get description for
*
* @return description of error
*
*/
string Utils::errnotostrerror(int errnum)
{
char buf[2048]="";
char *strerr;
// if(strerror_r(errnum,strerr,1024)!=-1)
#ifndef WIN32
strerr=strerror_r(errnum,buf,2048);
#else
strerr="Error ";
#endif //WIN32
return string(strerr)+" ("+inttostr(errnum)+")";
// else
// return string("Error "+inttostr(errno)+" retrieving error code for error number ")+inttostr(errnum);
}
/**
* returns whether the ip is in a dns list or not
*
* to check a dns list, you just append at the begining of the
* dns string the inversed ip and then resolve it. for example,
* to check the ip 1.2.3.4 you just resolve the following
* hostname:
* 4.3.2.1.zen.spamhaus.org
*
* if it returns an ip, then it is listed, else it isn't.
*
* since 1.4 this doesn't check a single domain but lists of them. we also added a percentage to decide when
* it's listed or not.
* So for example, if you want to check if you are on zen.spamhaus.org AND on dnsbl.sorbs.net, you have two options:
* - you need your ip to be listed on both domains to be considered as listed (100% of the lists)
* - you need your ip to be listed on either of them (50% of the lists)
*
* @param dns_domains list of dns domains to check
* @param percentage percentage of domains that have to list the ip to be considered as "listed"
* @param ip ip to check
*
* @return whether ip is blacklisted or not
*/
bool Utils::listed_on_dns_lists(list<string>& dns_domains,unsigned char percentage,string& ip)
{
string reversedip;
unsigned char number_of_lists=dns_domains.size();
unsigned char times_listed=0;
unsigned char checked_lists=0;
reversedip=reverseip(ip);
for(list<string>::iterator i=dns_domains.begin();i!=dns_domains.end();i++)
{
string dns_domain;
dns_domain=*i;
//add a dot if domain doesn't include it
if('.'!=dns_domain[0])
dns_domain.insert(0,".");
try
{
Socket::resolve(reversedip+dns_domain);
times_listed++;
#ifdef REALLY_VERBOSE_DEBUG
cout << ip << " listed on " << dns_domain << endl;
#endif //REALLY_VERBOSE_DEBUG
}
catch(Exception &e)
{
#ifdef REALLY_VERBOSE_DEBUG
cout << ip << " NOT listed on " << dns_domain << endl;
#endif //REALLY_VERBOSE_DEBUG
}
checked_lists++;
if((times_listed*100/number_of_lists)>=percentage)
return true;
//if we have checked a number of lists that make it impossible for this function
//to return true, then return false
if((checked_lists*100/number_of_lists)>100-percentage)
return false;
}
return false;
}
/**
* reverse an ip string from 1.2.3.4 to 4.3.2.1
*
* @param ip ip to reverse
*
* @return the reversed ip
*/
string Utils::reverseip(string& ip)
{
string inverseip="";
string::size_type pos=0,ppos=0;
//find first digit
pos=ip.rfind('.',ip.length());
if(string::npos==pos)
throw Exception(ip+" is not an IP",__FILE__,__LINE__);
else
inverseip=ip.substr(pos+1);
//find second digit
ppos=pos;
pos=ip.rfind('.',ppos-1);
if(string::npos==pos)
throw Exception(ip+" is not an IP",__FILE__,__LINE__);
else
inverseip+="."+ip.substr(pos+1,ppos-pos-1);
//find third digit
ppos=pos;
pos=ip.rfind('.',ppos-1);
if(string::npos==pos)
throw Exception(ip+" is not an IP",__FILE__,__LINE__);
else
inverseip+="."+ip.substr(pos+1,ppos-pos-1);
//append fourth digit
inverseip+="."+ip.substr(0,pos);
return inverseip;
}
/**
* get the time formatted according to rfc2821 and rfc2822
*
* @param timestamp time from which to return string. if null, it equals time(NULL)
*
* @return a formatted string of the form 18 May 2007 11:01:52 -0000
*/
string Utils::rfc2821_date(time_t *timestamp)
{
time_t utctime;
char buf[100]; //TODO:100 bytes should be enough, check though
char tzbuf[6]; //max length of a tz string is 5 (i.e. -0800)
struct tm local_time;
struct tm *p_local_time;
p_local_time=&local_time;
if(NULL==timestamp)
utctime=time(NULL);
else
utctime=*timestamp;
#ifdef WIN32
if(NULL==(p_local_time=localtime(&utctime)))
#else
if(NULL==(localtime_r(&utctime,&local_time)))
#endif //WIN32
throw Exception(_("Error converting date"),__FILE__,__LINE__);
if(0==strftime(buf,sizeof(buf),"%a, %d %b %Y %H:%M:%S ",p_local_time))
throw Exception(_("Error formatting date"),__FILE__,__LINE__);
#ifdef WIN32
//win32 doesn't return the same for %z on strftime, so do it manually
_tzset();
snprintf(tzbuf,sizeof(tzbuf),"%c%02d%02d",(-_timezone<0)?'-':'+',abs((_timezone/60)/60),abs((_timezone/60)%60));
#else
if(0==strftime(tzbuf,sizeof(tzbuf),"%z",p_local_time))
throw Exception(_("Error formatting timezone"),__FILE__,__LINE__);
#endif //WIN32
return string(buf)+string(tzbuf);
}
//hack for machines not defining HOST_NAME_MAX
//according to docs, if it's not defined, it SHOULD be 255
#ifndef HOST_NAME_MAX
#define HOST_NAME_MAX 255
#endif //HOST_NAME_MAX
/**
* get the hostname of the computer we are running at
*
* since this will be done for each message sent, and hostname shouldn't change during
* the execution of hermes, we will simply get it the first time and then we will store
* it on a static variable
*
* added the option to set the hostname on the configfile. should be useful on windows,
* where gethostname only returns the host part
*
* @return the hostname of the computer
*/
string Utils::gethostname()
{
static char buf[HOST_NAME_MAX]={0};
if('\0'==buf[0])
{
if(cfg.getHostname()!="")
strncpy(buf,cfg.getHostname().c_str(),HOST_NAME_MAX);
else
{
if(-1==::gethostname(buf,HOST_NAME_MAX))
throw Exception("Error getting current hostname"+Utils::errnotostrerror(errno),__FILE__,__LINE__);
}
}
return string(buf);
}

84
src/Utils.h Normal file
View file

@ -0,0 +1,84 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#ifndef UTILS_H
#define UTILS_H
#include "hermes.h"
#include <string>
#include <sstream>
#include <iostream>
#include <dirent.h>
#include <time.h>
#ifndef WIN32
#include <pwd.h>
#include <grp.h>
#endif //WIN32
#include "Database.h"
#include "Socket.h"
using namespace std;
#ifdef WIN32
#define sleep(x) Sleep(1000*(x))
#endif //WIN32
/*#ifndef MAX
#define MAX(x,y) (((x)>(y))?(x):(y))
#endif //MAX*/
/**
* this class implements common utilities
*/
class Utils
{
public:
//string utilities
static string strtolower(string);
static string trim(string);
static string inttostr(int);
//email-related utilities
static string getmail(string&);
static string getdomain(string&);
static string reverseip(string&);
//spam-related utilities (TODO: move to a different class)
static bool greylist(string,string&,string&,string&);
static bool listed_on_dns_lists(list<string>&,unsigned char,string&);
static bool whitelisted(string,string&);
static bool blacklisted(string,string&,string&);
#ifndef WIN32
//posix-utils
static int usertouid(string);
static int grouptogid(string);
#endif //WIN32
//misc
static bool file_exists(string);
static bool dir_exists(string);
static string errnotostrerror(int);
static string rfc2821_date(time_t *timestamp=NULL);
static string gethostname();
};
#endif //UTILS_H

413
src/hermes.cpp Normal file
View file

@ -0,0 +1,413 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#include <iostream>
#include <list>
#include <stack>
#include <pthread.h>
#include <signal.h>
#include <time.h>
#ifdef HAVE_SSL
#include <openssl/crypto.h>
#endif //HAVE_SSL
#ifndef WIN32
#include <grp.h>
#endif //WIN32
#include "Proxy.h"
#include "Socket.h"
#include "ServerSocket.h"
#include "Configfile.h"
#include "Utils.h"
#include "Logger.h"
using namespace std;
void *thread_main(void *);
void *cleaner_thread_run(void *);
void exit_requested(int);
void write_pid(string,pid_t);
//global var to know when we have to exit
bool quit=false;
//mutexes
pthread_mutex_t childrenlist_mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t info_stack_mutex=PTHREAD_MUTEX_INITIALIZER;
//our config
Configfile cfg;
//our logger
LOGGER_CLASS hermes_log;
list<pthread_t> children;
#ifdef HAVE_SSL
pthread_mutex_t ssl_locks[CRYPTO_NUM_LOCKS]={PTHREAD_MUTEX_INITIALIZER};
void ssl_locking_function(int mode,int n,const char *file,int line)
{
if(n>CRYPTO_NUM_LOCKS)
throw Exception(_("Error, "+Utils::inttostr(n)+" is bigger than CRYPTO_NUM_LOCKS("+Utils::inttostr(CRYPTO_NUM_LOCKS)+")"),__FILE__,__LINE__);
if(mode&CRYPTO_LOCK)
pthread_mutex_lock(&ssl_locks[n]);
else
pthread_mutex_unlock(&ssl_locks[n]);
}
#endif //HAVE_SSL
int
#ifdef WIN32_SERVICE
hermes_main
#else
main
#endif //WIN32_SERVICE
(int argc,char *argv[])
{
/* TODO:think of this again
if(argc>2)
{
for(unsigned i=1;i<argc;i++)
{
argv++
}
*/
#ifdef HAVE_SSL
CRYPTO_set_locking_callback(ssl_locking_function);
CRYPTO_set_id_callback(pthread_self);
#endif //HAVE_SSL
try
{
if(2==argc)
{
if(!Utils::file_exists(argv[1]))
throw Exception(string(_("Config file "))+argv[1]+_(" doesn't exist or is not readable."),__FILE__,__LINE__);
cfg.parse(argv[1]);
}
cfg.validateConfig();
}
catch(Exception &e)
{
cout << string(e) << endl;
return -1;
}
#ifdef REALLY_VERBOSE_DEBUG
unsigned long nconns=0;
#endif //REALLY_VERBOSE_DEBUG
signal(SIGTERM,exit_requested);
signal(SIGINT,exit_requested);
//we have to create the server socket BEFORE chrooting, because if we don't,
//SSL cannot initialize because it's missing libz
ServerSocket server;
pthread_t cleaner_thread;
string peer_address;
#ifndef WIN32
if(cfg.getBackground())
{
int retval;
retval=fork();
if(retval>0)
exit(0); //succesful fork
if(retval<0)
{
cout << _("Error forking into the background") << Utils::errnotostrerror(errno) << endl;
return -1;
}
}
if(cfg.getPidFile()!="")
{
try
{
write_pid(cfg.getPidFile(),getpid());
}
catch(Exception &e)
{
hermes_log.addMessage(LOG_ERR,e);
}
}
if(cfg.getChroot()!="")
{
//this is needed to get hermes to load the dns resolver BEFORE chrooting
(void)gethostbyname("iteisa.com");
chdir(cfg.getChroot().c_str());
if(-1==chroot(cfg.getChroot().c_str()))
{
cout << _("Couldn't chroot ") << Utils::errnotostrerror(errno) << endl;
return -1;
}
chdir("/");
}
#endif //WIN32
try
{
server.init();
server.setPort(cfg.getListeningPort());
server.listen(cfg.getListeningPort(),cfg.getBindTo());
}
catch(Exception &e)
{
cout << e << endl;
return -1; //couldn't bind, exit
}
#ifndef WIN32
if(cfg.getDropPrivileges())
{
//drop privileges once we have opened the listening port
setgroups(0,NULL);
setgid(cfg.getGid());
setuid(cfg.getUid());
setuid(cfg.getUid());
}
#endif //WIN32
/* start our cleaner thread */
if(cfg.getCleanDb())
pthread_create(&cleaner_thread,NULL,cleaner_thread_run,NULL);
new_conn_info info;
stack<new_conn_info> info_stack;
while(!quit)
{
if(server.canRead(1)) //wait one second for incoming connections, if none then loop again(allows us to check for SIGTERM and SIGINT)
{
pthread_t thread;
pthread_attr_t thread_attr;
pthread_attr_init(&thread_attr);
pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED);
int retval;
int fd=server.accept(&peer_address);
info.new_fd=fd;
info.peer_address=peer_address;
pthread_mutex_lock(&info_stack_mutex);
info_stack.push(info);
pthread_mutex_unlock(&info_stack_mutex);
retval=pthread_create(&thread,&thread_attr,thread_main,(void *)&info_stack);
if(retval)
{
#ifdef REALLY_VERBOSE_DEBUG
cout << _("Error creating thread: ") << Utils::errnotostrerror(retval) << _(". Sleeping 5 seconds before continuing...") << endl;
#endif //REALLY_VERBOSE_DEBUG
sleep(5);
}
else
{
#ifdef REALLY_VERBOSE_DEBUG
cout << "[ " << ++nconns << " ] " << endl;
#endif //REALLY_VERBOSE_DEBUG
pthread_mutex_lock(&childrenlist_mutex);
children.push_back(thread);
pthread_mutex_unlock(&childrenlist_mutex);
}
}
}
//close connection so that the port is no longer usable
server.close();
// wait for all threads to finish
#ifdef REALLY_VERBOSE_DEBUG
cout << "Waiting for all threads to finish" << endl;
#endif //REALLY_VERBOSE_DEBUG
while(children.size())
{
#ifdef REALLY_VERBOSE_DEBUG
cout << "Threads active:" << children.size() << (char)13;
fflush(stdout);
#endif //REALLY_VERBOSE_DEBUG
sleep(1);
}
if(cfg.getCleanDb())
pthread_join(cleaner_thread,NULL);
#ifdef REALLY_VERBOSE_DEBUG
cout << endl;
#endif //REALLY_VERBOSE_DEBUG
#ifdef HAVE_SPF
Spf::deinitialize();
#endif //HAVE_SPF
return 0;
}
/**
* this threads cleans the database once each hour, deleting
* the records on the database that have an expire time < now
*
*/
void *cleaner_thread_run(void *)
{
try
{
Database db;
time_t next_run=time(NULL)+3600;
db.setDatabaseFile(cfg.getDatabaseFile());
db.open();
while(!quit)
{
time_t now=time(NULL);
sched_yield();
if(now>next_run)
{
unsigned long spamcount;
spamcount=db.cleanDB();
hermes_log.addMessage(LOG_DEBUG,"Cleaning database, cleaning "+Utils::inttostr(spamcount)+" blocked spams.");
if(spamcount>0&&cfg.getSubmitStats())
{
try
{
Socket s;
string server_response;
s.init();
s.connect("hermes-stats.iteisa.com",11125);
#ifdef HAVE_SSL
if(cfg.getSubmitStatsSsl())
{
s.writeLine("ssl");
s.enableSSL(false);
}
else
#endif //HAVE_SSL
s.writeLine("non-ssl");
s.writeLine(cfg.getSubmitStatsUsername());
s.writeLine(cfg.getSubmitStatsPassword());
s.writeLine(Utils::inttostr(spamcount));
server_response=s.readLine();
s.close();
if("OK"!=server_response)
throw Exception(server_response,__FILE__,__LINE__);
}
catch(Exception &e)
{
hermes_log.addMessage(LOG_DEBUG,"Exception sending stats: "+string(e));
}
}
next_run+=3600;
}
#ifdef REALLY_VERBOSE_DEBUG
if(!(now%10)) //echo info each 10 seconds
{
stringstream ss;
pthread_mutex_lock(&childrenlist_mutex);
ss << children.size() << " threads running: ";
for(list<pthread_t>::iterator i=children.begin();i!=children.end();i++)
ss << "[ " << *i << " ] ";
pthread_mutex_unlock(&childrenlist_mutex);
ss << endl;
cout << ss.str();
}
#endif //REALLY_VERBOSE_DEBUG
sleep(1);
}
db.close();
}
catch(Exception &e)
{
#ifdef REALLY_VERBOSE_DEBUG
cout << e << endl;
#endif //REALLY_VERBOSE_DEBUG
}
return NULL;
}
void remove_thread_from_list(pthread_t thread)
{
pthread_mutex_lock(&childrenlist_mutex);
children.remove(thread);
pthread_mutex_unlock(&childrenlist_mutex);
}
void *thread_main(void *info_stack)
{
try
{
Socket client; //for the input connection from the client
Proxy p;
new_conn_info peerinfo;
//read a new peerinfo from the stack
pthread_mutex_lock(&info_stack_mutex);
peerinfo=((stack<new_conn_info>*)info_stack)->top();
((stack<new_conn_info>*)info_stack)->pop();
pthread_mutex_unlock(&info_stack_mutex);
client.setFD(peerinfo.new_fd);
p.setOutside(client);
p.run(peerinfo.peer_address);
remove_thread_from_list(pthread_self());
}
catch(Exception &e)
{
#ifdef REALLY_VERBOSE_DEBUG
cout << e << endl;
#endif //REALLY_VERBOSE_DEBUG
hermes_log.addMessage(LOG_DEBUG,e);
}
return NULL;
}
void exit_requested(int)
{
if(!quit)
{
quit=true;
#ifdef REALLY_VERBOSE_DEBUG
cout << "Hit control+c again to force-quit" << endl;
#endif //REALLY_VERBOSE_DEBUG
}
else
exit(-1);
}
void write_pid(string file,pid_t pid)
{
FILE *f;
f=fopen(file.c_str(),"w");
if(NULL==f)
throw Exception(_("Couldn't open file ")+file+_(" to write the pidfile"),__FILE__,__LINE__);
fprintf(f,"%d\n",pid);
fclose(f);
}
#ifdef WIN32
//pthreads on win32 doesn't provide an operator== for pthread_t
//and it's also an struct, not an int, so supply one operator== here
bool operator==(pthread_t t1,pthread_t t2)
{
return t1.p==t2.p&&t1.x==t2.x;
}
#endif //WIN32

36
src/hermes.h Normal file
View file

@ -0,0 +1,36 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#ifndef SMTPPROXY_H
#define SMTPPROXY_H
#include "config.h"
#include "Exception.h"
#include <assert.h>
#include <string>
#define _(x) (x)
typedef struct
{
int new_fd;
std::string peer_address;
}new_conn_info;
#endif //SMTPPROXY_H

264
src/win32-service.cpp Normal file
View file

@ -0,0 +1,264 @@
/**
* hermes antispam proxy
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
*/
#include <string>
#include <windows.h>
using namespace std;
#define SERVICE_NAME "hermes anti-spam proxy"
#define SERVICE_SHORT_NAME "hermes"
#define SERVICE_DESCRIPTION_TEXT "An anti-spam proxy using a combination of techniques like greylisting, dnsbl/dnswl, SPF, etc."
//macros
#define ChangeServiceStatus(x,y,z) y.dwCurrentState=z; SetServiceStatus(x,&y);
#define MIN(x,y) (((x)<(y))?(x):(y))
#define cmp(x,y) strncmp(x,y,strlen(y))
#define msgbox(x,y,z) MessageBox(NULL,x,z SERVICE_NAME,MB_OK|y)
#define winerror(x) msgbox(x,MB_ICONERROR,"Error from ")
#define winmessage(x) msgbox(x,MB_ICONINFORMATION,"Message from ")
#define condfree(x) if(NULL!=x) free(x);
#define safemalloc(x,y,z) do { if(NULL==(x=(z)malloc(y))) { winerror("Error reserving memory"); exit(-1); } memset(x,0,y); } while(0)
#define _(x) x
/**
* The docs on microsoft's web don't seem very clear, so I have
* looked at the stunnel source code to understand how this thing
* works. What you see here is still original source, but is
* "inspired" by stunnel's source code (gui.c mainly).
* It's the real minimum needed to install, start and stop services
*/
extern bool quit;
extern int hermes_main(int,char**);
SERVICE_STATUS service_status;
SERVICE_STATUS_HANDLE service_status_handle;
int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int);
static void WINAPI service_main(DWORD,LPTSTR*);
static void WINAPI handler(DWORD);
static int service_install();
static int service_uninstall();
char **params=NULL;
#define free_params() \
do \
{ \
if(NULL!=params) \
{ \
condfree(params[0]); \
condfree(params[1]); \
} \
condfree(params); \
} \
while(0)
#define init_params() \
do \
{ \
free_params(); \
safemalloc(params,sizeof(char *)*2,char **); \
safemalloc(params[0],1*sizeof(char),char *); \
params[0][0]='\0'; \
safemalloc(params[1],1024*sizeof(char),char *); \
} \
while(0)
int WINAPI WinMain(HINSTANCE instance,HINSTANCE previous_instance,LPSTR cmdline,int cmdshow)
{
if(!cmp(cmdline,"-service"))
{
SERVICE_TABLE_ENTRY service_table[]={
{SERVICE_SHORT_NAME,service_main},
{NULL,NULL}
};
if(0==StartServiceCtrlDispatcher(service_table))
{
winerror("Error starting service dispatcher.");
return -1;
}
}
else if(!cmp(cmdline,"-install"))
service_install();
else if(!cmp(cmdline,"-uninstall"))
service_uninstall();
else
{
//we know that hermes can only have one parameter, so
//just copy it
init_params();
strncpy(params[1],cmdline,1024);
hermes_main(2,(char **)params);
free_params();
}
return 0;
}
static int service_install()
{
SC_HANDLE scm,service_handle;
SERVICE_DESCRIPTION service_description;
char filename[1024];
string exepath;
if(NULL==(scm=OpenSCManager(NULL,NULL,SC_MANAGER_CREATE_SERVICE)))
{
winerror(_("Error opening connection to the Service Manager."));
exit(-1);
}
if(0==GetModuleFileName(NULL,filename,sizeof(filename)))
{
winerror(_("Error getting the file name of the process."));
exit(-1);
}
exepath=string("\"")+filename+"\" -service";
service_handle=CreateService(
scm, //scm handle
SERVICE_SHORT_NAME, //service name
SERVICE_NAME, //display name
SERVICE_ALL_ACCESS, //desired access
SERVICE_WIN32_OWN_PROCESS, //service type
SERVICE_AUTO_START, //start type
SERVICE_ERROR_NORMAL, //error control
exepath.c_str(), //executable path with arguments
NULL, //load group
NULL, //tag for group id
NULL, //dependencies
NULL, //user name
NULL); //password
if(NULL==service_handle)
{
winerror("Error creating service. Already installed?");
exit(-1);
}
else
winmessage("Service successfully installed.");
//createservice doesn't have a field for description
//so we use ChangeServiceConfig2
service_description.lpDescription=SERVICE_DESCRIPTION_TEXT;
ChangeServiceConfig2(service_handle,SERVICE_CONFIG_DESCRIPTION,(void *)&service_description);
CloseServiceHandle(service_handle);
CloseServiceHandle(scm);
return 0;
}
static int service_uninstall()
{
SC_HANDLE scm,service_handle;
SERVICE_STATUS status;
if(NULL==(scm=OpenSCManager(NULL,NULL,SC_MANAGER_CREATE_SERVICE)))
{
winerror(_("Error opening connection to the Service Manager."));
exit(-1);
}
if(NULL==(service_handle=OpenService(scm,SERVICE_SHORT_NAME,SERVICE_QUERY_STATUS|DELETE)))
{
winerror(_("Error opening service."));
CloseServiceHandle(scm);
exit(-1);
}
if(0==QueryServiceStatus(service_handle,&status))
{
winerror(_("Error querying service."));
CloseServiceHandle(scm);
CloseServiceHandle(service_handle);
exit(-1);
}
if(SERVICE_STOPPED!=status.dwCurrentState)
{
winerror(SERVICE_NAME _(" is still running. Stop it before trying to uninstall it."));
CloseServiceHandle(scm);
CloseServiceHandle(service_handle);
exit(-1);
}
if(0==DeleteService(service_handle))
{
winerror(_("Error deleting service."));
CloseServiceHandle(scm);
CloseServiceHandle(service_handle);
exit(-1);
}
CloseServiceHandle(scm);
CloseServiceHandle(service_handle);
winmessage(_("Service successfully uninstalled."));
return 0;
}
static void WINAPI service_main(DWORD argc,LPTSTR *argv)
{
char *tmpstr;
//configure service_status structure
service_status.dwServiceType=SERVICE_WIN32;
service_status.dwControlsAccepted=0;
service_status.dwWin32ExitCode=NO_ERROR;
service_status.dwServiceSpecificExitCode=NO_ERROR;
service_status.dwCheckPoint=0;
service_status.dwWaitHint=0;
service_status.dwControlsAccepted|=SERVICE_ACCEPT_STOP;
service_status_handle=RegisterServiceCtrlHandler(SERVICE_SHORT_NAME,handler);
if(0!=service_status_handle)
{
//set service status
ChangeServiceStatus(service_status_handle,service_status,SERVICE_RUNNING);
//get the path to the config file
init_params();
GetModuleFileName(NULL,params[1],1024);
if(NULL==(tmpstr=strrchr(params[1],'\\'))) { winerror("Error finding default config file."); exit(-1); }
*(++tmpstr)='\0';
strncat(params[1],"hermes.ini",strlen("hermes.ini"));
//now start our main program
hermes_main(2,(char **)params);
free_params();
//when we are here, we have been stopped
ChangeServiceStatus(service_status_handle,service_status,SERVICE_STOP_PENDING);
ChangeServiceStatus(service_status_handle,service_status,SERVICE_STOPPED);
}
}
static void WINAPI handler(DWORD code)
{
if(SERVICE_CONTROL_STOP==code)
{
quit=true;
ChangeServiceStatus(service_status_handle,service_status,SERVICE_STOP_PENDING);
}
}