diff --git a/.drone.yml b/.drone.yml index 76e74e9..f2a51bb 100644 --- a/.drone.yml +++ b/.drone.yml @@ -16,7 +16,7 @@ steps: image: alpine commands: - apk add -t hermes-build-deps --no-cache perl graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev - - cmake -B build_dir -D BUILD_DOCS=ON + - cmake -B build_dir - cmake --build build_dir - name: docker image build image: plugins/docker diff --git a/CMakeLists.txt b/CMakeLists.txt index 257ad0e..c9d814d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,8 +14,6 @@ add_executable (hermes src/Proxy.cpp src/Socket.cpp) -option(BUILD_DOC "Build documentation") - if(WIN32) set(SOURCES ${SOURCES} src/FileLogger.cpp @@ -58,16 +56,14 @@ add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Configfile.cpp # doxygen -if (BUILD_DOCS) - find_package (Doxygen) - if(DOXYGEN_FOUND) - add_custom_target(doc ALL - doxygen - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/docs) - install( - DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/docs/html - TYPE DOC) - endif() +find_package (Doxygen REQUIRED dot) +if(DOXYGEN_FOUND) + add_custom_target(doc ALL + doxygen + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/docs) + install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/docs/html + TYPE DOC) endif() target_link_libraries(hermes diff --git a/src/Proxy.cpp b/src/Proxy.cpp index 1515869..319417c 100644 --- a/src/Proxy.cpp +++ b/src/Proxy.cpp @@ -1,233 +1,338 @@ -// Proxy.cpp +/** + * 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" -#include -#include -#include "Utils.h" // Dummy include; replace with actual Utils implementation -#include "Configfile.h" // Dummy include; replace with actual Configfile implementation -extern Configfile cfg; // External configuration +extern LOGGER_CLASS hermes_log; +extern Configfile cfg; -void Proxy::run(std::string& peer_address) { - // Original comments and variables retained - std::string from = ""; - std::string to = ""; - std::string ehlostr = ""; - std::string resolvedname = ""; - unsigned char last_state = SMTP_STATE_WAIT_FOR_HELO; - long unimplemented_requests = 0; - - try { - bool throttled = cfg.getThrottle(); // Start with a throttled connection - bool authenticated = false; // Start with a non-authenticated connection - bool esmtp = false; - std::string strtemp; - std::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 (cfg.getWhitelistedDisablesEverything()) { - throttled = false; - } - } - - if (cfg.getWhitelistedDisablesEverything() && Utils::whitelisted(cfg.getDatabaseFile(), peer_address)) { - throttled = false; - authenticated = true; - } else { - if (!cfg.getAllowDataBeforeBanner()) { - std::this_thread::sleep_for(std::chrono::seconds(cfg.getBannerDelayTime())); - if (outside->canRead(0)) { // if we have data waiting before the server gives us a 220 - std::cout << "421 (data_before_banner) (ip:" << peer_address << ")\n"; // Log it - std::this_thread::sleep_for(std::chrono::seconds(20)); // Annoy spammers once more - outside->writeLine("421 Stop sending data before we show you the banner"); - return; - } - } - } - - // Connect to the inside server - 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 - - // Main loop for communication - while (!outside->isClosed() && !inside.isClosed()) { - // Check if the client wants to send something to the server - if (outside->canRead(0.2)) { - 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))) { - std::string strlog = ""; - std::string code = ""; - std::string mechanism = ""; - std::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)) { - code = "421"; - mechanism = "greylist"; - message = code + " Greylisted!! Please try again in a few minutes."; - std::cout << "checking " << mechanism << "\n"; - } - #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."; - std::cout << "checking " << mechanism << "\n"; - } - #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."; - std::cout << "checking " << mechanism << "\n"; - } - // 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."; - std::cout << "checking " << mechanism << "\n"; - } - 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."; - std::cout << "checking " << mechanism << "\n"; - } - 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."; - std::cout << "checking " << mechanism << "\n"; - } else { - code = "250"; - } - - if (!mechanism.empty()) strlog.insert(0, "(" + mechanism + ") "); - strlog.insert(0, code + " "); - std::cout << strlog << "\n"; // Log the connection - - // 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 - std::this_thread::sleep_for(std::chrono::seconds(20)); - outside->writeLine(message); - return; - } - last_state = SMTP_STATE_WAIT_FOR_DATA; - } - - // Handle STARTTLS - if ("starttls" == Utils::strtolower(strtemp.substr(0, 8))) { - #ifdef HAVE_SSL - try { - outside->prepareSSL(true); - std::cout << "STARTTLS issued by remote, TLS enabled\n"; - outside->writeLine("220 You can speak now, line is secure!!"); - outside->startSSL(true); - } catch (Exception& e) { - std::cout << "STARTTLS issued by remote, but enableSSL failed!\n"; - outside->writeLine("454 Tried to enable SSL but failed"); - } - #else - outside->writeLine("454 TLS temporarily not available"); - std::cout << "STARTTLS issued by remote, TLS was not enabled because this build lacks SSL support\n"; - #endif // HAVE_SSL - strtemp = ""; - } - - if (strtemp.length()) inside.writeLine(strtemp); - } - - // Check if the server wants to send something to the client - if (inside.canRead(0.2)) { - strtemp = inside.readLine(); - if (inside.isClosed()) return; - - std::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 - std::string endofdata = ""; - ssize_t bytes_read = 0; - char buffer[4097]; - outside->writeLine(strtemp); - - 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 += std::string(buffer); - else endofdata = std::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 - throttled = false; - authenticated = true; - hermes_status = "authenticated"; - } - - // Try to annoy spammers who send too many senseless commands by delaying their connection - if ("502" == code) { // 502 unimplemented - if (cfg.getNumberOfUnimplementedCommandsAllowed() != -1 && ++unimplemented_requests > cfg.getNumberOfUnimplementedCommandsAllowed()) { - inside.writeLine("QUIT"); - inside.close(); // Close the socket now and leave server alone - std::this_thread::sleep_for(std::chrono::seconds(60)); - outside->writeLine("502 Too many unimplemented commands, closing connection"); - return; - } - } - - if (strtemp.length()) outside->writeLine(strtemp); - } - - if (throttled) std::this_thread::sleep_for(std::chrono::seconds(cfg.getThrottlingTime())); // Take 1 second between each command - } - } catch (Exception& e) { // Any exception will close both connections - std::cerr << "Exception occurred: " << e.what() << std::endl; - return; - } +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; + } }