337 lines
12 KiB
C++
337 lines
12 KiB
C++
/**
|
|
* 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 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;
|
|
long unimplemented_requests=0;
|
|
|
|
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;
|
|
string hermes_status="unknown";
|
|
|
|
//check whitelist
|
|
if(!cfg.getDnsWhitelistDomains().empty()&&Utils::listed_on_dns_lists(cfg.getDnsWhitelistDomains(),cfg.getDnsWhitelistPercentage(),peer_address))
|
|
{
|
|
authenticated=true;
|
|
hermes_status="whitelisted";
|
|
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
|
|
{
|
|
LINF("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.prepareSSL(false);
|
|
inside.startSSL(false);
|
|
}
|
|
if(cfg.getIncomingSsl())
|
|
{
|
|
outside.prepareSSL(true);
|
|
outside.startSSL(true);
|
|
}
|
|
#endif //HAVE_SSL
|
|
|
|
while(!outside.isClosed()&&!inside.isClosed())
|
|
{
|
|
if(outside.canRead(0.2)) //client wants to send something to server
|
|
{
|
|
strtemp=outside.readLine();
|
|
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.";
|
|
LINF("checking " + mechanism);
|
|
}
|
|
#ifdef HAVE_SPF
|
|
else if(cfg.getQuerySpf()&&!authenticated&&!spf_checker.query(peer_address,ehlostr,from))
|
|
{
|
|
hermes_status="spf-failed";
|
|
if(cfg.getAddStatusHeader())
|
|
code="250";
|
|
else
|
|
code=cfg.getReturnTempErrorOnReject()?"421":"550";
|
|
mechanism="spf";
|
|
message=code+" You do not seem to be allowed to send email for that particular domain.";
|
|
LINF("checking " + mechanism);
|
|
}
|
|
#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.";
|
|
LINF("checking " + mechanism);
|
|
}
|
|
//check rbl
|
|
else if(!cfg.getDnsBlacklistDomains().empty()&&!authenticated&&Utils::listed_on_dns_lists(cfg.getDnsBlacklistDomains(),cfg.getDnsBlacklistPercentage(),peer_address))
|
|
{
|
|
hermes_status="blacklisted";
|
|
if(cfg.getAddStatusHeader())
|
|
code="250";
|
|
else
|
|
code=cfg.getReturnTempErrorOnReject()?"421":"550";
|
|
mechanism="dnsbl";
|
|
message=code+" You are listed on some DNS blacklists. Get delisted before trying to send us email.";
|
|
LINF("checking " + mechanism);
|
|
}
|
|
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.";
|
|
LINF("checking " + mechanism);
|
|
}
|
|
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.";
|
|
LINF("checking " + mechanism);
|
|
}
|
|
else
|
|
code="250";
|
|
|
|
if(""!=mechanism)
|
|
strlog.insert(0,"("+mechanism+") ");
|
|
strlog.insert(0,code+" ");
|
|
|
|
//log the connection
|
|
LINF(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
|
|
{
|
|
outside.prepareSSL(true);
|
|
LINF("STARTTLS issued by remote, TLS enabled");
|
|
outside.writeLine("220 You can speak now, line is secure!!");
|
|
outside.startSSL(true);
|
|
}
|
|
catch(Exception &e)
|
|
{
|
|
LINF("STARTTLS issued by remote, but enableSSL failed!");
|
|
LERR(e);
|
|
outside.writeLine("454 Tried to enable SSL but failed");
|
|
}
|
|
#else
|
|
outside.writeLine("454 TLS temporarily not available");
|
|
LINF("STARTTLS issued by remote, TLS was not enabled because this build lacks SSL support");
|
|
#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();
|
|
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="";
|
|
string ssltls="";
|
|
if (outside.is_ssl_enabled())
|
|
ssltls=" (SSL/TLS)";
|
|
|
|
if(cfg.getAddHeaders())
|
|
{
|
|
inside.writeLine("Received: from "+ehlostr+" ("+peer_address+")");
|
|
inside.writeLine(" by "+Utils::gethostname(outside.getFD())+" with "+(esmtp?"ESTMP":"SMTP")+ssltls+" via TCP; "+Utils::rfc2821_date());
|
|
inside.writeLine("X-Anti-Spam-Proxy: Proxied by Hermes [www.hermes-project.com]");
|
|
if(cfg.getAddStatusHeader())
|
|
inside.writeLine("X-Hermes-Status: "+hermes_status);
|
|
}
|
|
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;
|
|
hermes_status="authenticated";
|
|
}
|
|
if("250-pipelining"==Utils::strtolower(strtemp)||"250-chunking"==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)||"250 chunking"==Utils::strtolower(strtemp))
|
|
strtemp="250 x-noextension";
|
|
|
|
//try to annoy spammers who send us too many senseless commands by delaying their connection a lot
|
|
if("502"==code) //502 unimplemented -> count them, if bigger than a certain number, terminate connection
|
|
{
|
|
if(cfg.getNumberOfUnimplementedCommandsAllowed()!=-1&&++unimplemented_requests>cfg.getNumberOfUnimplementedCommandsAllowed())
|
|
{
|
|
inside.writeLine("QUIT");
|
|
inside.close(); //close the socket now and leave server alone
|
|
sleep(60);
|
|
outside.writeLine("502 Too many unimplemented commands, closing connection");
|
|
return;
|
|
}
|
|
}
|
|
|
|
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
|
|
{
|
|
LERR(e);
|
|
if(last_state<SMTP_STATE_WAIT_FOR_DATA)
|
|
LINF("421 (probably-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;
|
|
}
|
|
}
|