/** * hermes antispam proxy * Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo * * 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 */ #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=""; #ifdef HAVE_SSL if (outside.is_ssl_enabled()) ssltls=" (SSL/TLS)"; #endif //HAVE_SSL 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 to "+(""==to?"no-to":to)); return; } }