Compare commits

..

No commits in common. "a4c68baa45a117061c934734c40fd5bb8875a679" and "6b610aaa84beb06df65d8ea42305d97a8c70b87f" have entirely different histories.

3 changed files with 343 additions and 242 deletions

View file

@ -16,7 +16,7 @@ steps:
image: alpine image: alpine
commands: commands:
- apk add -t hermes-build-deps --no-cache perl graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev - 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 - cmake --build build_dir
- name: docker image build - name: docker image build
image: plugins/docker image: plugins/docker

View file

@ -14,8 +14,6 @@ add_executable (hermes
src/Proxy.cpp src/Proxy.cpp
src/Socket.cpp) src/Socket.cpp)
option(BUILD_DOC "Build documentation")
if(WIN32) if(WIN32)
set(SOURCES ${SOURCES} set(SOURCES ${SOURCES}
src/FileLogger.cpp src/FileLogger.cpp
@ -58,8 +56,7 @@ add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Configfile.cpp
# doxygen # doxygen
if (BUILD_DOCS) find_package (Doxygen REQUIRED dot)
find_package (Doxygen)
if(DOXYGEN_FOUND) if(DOXYGEN_FOUND)
add_custom_target(doc ALL add_custom_target(doc ALL
doxygen doxygen
@ -68,7 +65,6 @@ if (BUILD_DOCS)
DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/docs/html DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/docs/html
TYPE DOC) TYPE DOC)
endif() endif()
endif()
target_link_libraries(hermes target_link_libraries(hermes
${SQLITE3_LIBRARY} ${SQLITE3_LIBRARY}

View file

@ -1,233 +1,338 @@
// Proxy.cpp /**
* 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" #include "Proxy.h"
#include <iostream>
#include <thread>
#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) { void Proxy::setOutside(Socket& p_outside)
// Original comments and variables retained {
std::string from = ""; outside=p_outside;
std::string to = ""; }
std::string ehlostr = "";
std::string resolvedname = ""; /**
* 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; unsigned char last_state=SMTP_STATE_WAIT_FOR_HELO;
long unimplemented_requests=0; long unimplemented_requests=0;
try { try
bool throttled = cfg.getThrottle(); // Start with a throttled connection {
bool authenticated = false; // Start with a non-authenticated connection bool throttled=cfg.getThrottle(); //we start with a throttled connection
bool authenticated=false; //we start with a non-authenticated connection
bool esmtp=false; bool esmtp=false;
std::string strtemp; string strtemp;
std::string hermes_status = "unknown"; string hermes_status="unknown";
// Check whitelist //check whitelist
if (!cfg.getDnsWhitelistDomains().empty() && if(!cfg.getDnsWhitelistDomains().empty()&&Utils::listed_on_dns_lists(cfg.getDnsWhitelistDomains(),cfg.getDnsWhitelistPercentage(),peer_address))
Utils::listed_on_dns_lists(cfg.getDnsWhitelistDomains(), cfg.getDnsWhitelistPercentage(), peer_address)) { {
authenticated=true; authenticated=true;
hermes_status="whitelisted"; hermes_status="whitelisted";
if (cfg.getWhitelistedDisablesEverything()) { if(true==cfg.getWhitelistedDisablesEverything())
throttled=false; throttled=false;
} }
} if(true==cfg.getWhitelistedDisablesEverything()&&Utils::whitelisted(cfg.getDatabaseFile(),peer_address))
{
if (cfg.getWhitelistedDisablesEverything() && Utils::whitelisted(cfg.getDatabaseFile(), peer_address)) {
throttled=false; throttled=false;
authenticated=true; authenticated=true;
} else { }
if (!cfg.getAllowDataBeforeBanner()) { else
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 if(false==cfg.getAllowDataBeforeBanner())
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 sleep(cfg.getBannerDelayTime());
outside->writeLine("421 Stop sending data before we show you the banner"); 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; return;
} }
} }
} }
// Connect to the inside server inside.init();
inside.connect(cfg.getServerHost(),cfg.getServerPort()); inside.connect(cfg.getServerHost(),cfg.getServerPort());
#ifdef HAVE_SSL #ifdef HAVE_SSL
if (cfg.getOutgoingSsl()) { if(cfg.getOutgoingSsl())
{
inside.prepareSSL(false); inside.prepareSSL(false);
inside.startSSL(false); inside.startSSL(false);
} }
if (cfg.getIncomingSsl()) { if(cfg.getIncomingSsl())
outside->prepareSSL(true); {
outside->startSSL(true); outside.prepareSSL(true);
outside.startSSL(true);
} }
#endif //HAVE_SSL #endif //HAVE_SSL
// Main loop for communication while(!outside.isClosed()&&!inside.isClosed())
while (!outside->isClosed() && !inside.isClosed()) { {
// Check if the client wants to send something to the server if(outside.canRead(0.2)) //client wants to send something to server
if (outside->canRead(0.2)) { {
strtemp = outside->readLine(); strtemp=outside.readLine();
if (outside->isClosed()) return; if(outside.isClosed())
return;
if (strtemp.length() > 10 && "mail from:" == Utils::strtolower(strtemp.substr(0, 10))) { if(strtemp.length()>10&&"mail from:"==Utils::strtolower(strtemp.substr(0,10)))
{
from=Utils::getmail(strtemp); from=Utils::getmail(strtemp);
last_state=SMTP_STATE_WAIT_FOR_RCPTTO; 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)) || if("ehlo"==Utils::strtolower(strtemp.substr(0,4)))
"helo" == 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)); ehlostr=Utils::trim(strtemp.substr(5));
last_state=SMTP_STATE_WAIT_FOR_MAILFROM; 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 { 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); resolvedname=Socket::resolveInverselyToString(peer_address);
} catch (Exception& e) { }
catch(Exception &e)
{
resolvedname=""; resolvedname="";
} }
strlog = "from " + from + " (ip:" + peer_address + ", hostname:" + resolvedname + strlog="from "+from+" (ip:"+peer_address+", hostname:"+resolvedname+", "+(esmtp?"ehlo":"helo")+":"+ehlostr+") -> to "+to;
", " + (esmtp ? "ehlo" : "helo") + ":" + ehlostr + ") -> to " + to;
// Check greylisting //check greylisting
if (cfg.getGreylist() && !authenticated && Utils::greylist(cfg.getDatabaseFile(), peer_address, from, to)) { 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"; code="421";
mechanism="greylist"; mechanism="greylist";
message=code+" Greylisted!! Please try again in a few minutes."; message=code+" Greylisted!! Please try again in a few minutes.";
std::cout << "checking " << mechanism << "\n"; LINF("checking " + mechanism);
} }
#ifdef HAVE_SPF #ifdef HAVE_SPF
else if (cfg.getQuerySpf() && !authenticated && !spf_checker.query(peer_address, ehlostr, from)) { else if(cfg.getQuerySpf()&&!authenticated&&!spf_checker.query(peer_address,ehlostr,from))
{
hermes_status="spf-failed"; hermes_status="spf-failed";
if (cfg.getAddStatusHeader()) code = "250"; if(cfg.getAddStatusHeader())
else code = cfg.getReturnTempErrorOnReject() ? "421" : "550"; code="250";
else
code=cfg.getReturnTempErrorOnReject()?"421":"550";
mechanism="spf"; mechanism="spf";
message=code+" You do not seem to be allowed to send email for that particular domain."; message=code+" You do not seem to be allowed to send email for that particular domain.";
std::cout << "checking " << mechanism << "\n"; LINF("checking " + mechanism);
} }
#endif //HAVE_SPF #endif //HAVE_SPF
// Check blacklist //check blacklist
else if (!authenticated && Utils::blacklisted(cfg.getDatabaseFile(), peer_address, to)) { else if(!authenticated&&Utils::blacklisted(cfg.getDatabaseFile(),peer_address,to))
{
code=cfg.getReturnTempErrorOnReject()?"421":"550"; code=cfg.getReturnTempErrorOnReject()?"421":"550";
mechanism="allowed-domain-per-ip"; mechanism="allowed-domain-per-ip";
message=code+" You do not seem to be allowed to send email to that particular domain from that address."; message=code+" You do not seem to be allowed to send email to that particular domain from that address.";
std::cout << "checking " << mechanism << "\n"; LINF("checking " + mechanism);
} }
// Check RBL //check rbl
else if (!cfg.getDnsBlacklistDomains().empty() && !authenticated && else if(!cfg.getDnsBlacklistDomains().empty()&&!authenticated&&Utils::listed_on_dns_lists(cfg.getDnsBlacklistDomains(),cfg.getDnsBlacklistPercentage(),peer_address))
Utils::listed_on_dns_lists(cfg.getDnsBlacklistDomains(), cfg.getDnsBlacklistPercentage(), peer_address)) { {
hermes_status="blacklisted"; hermes_status="blacklisted";
if (cfg.getAddStatusHeader()) code = "250"; if(cfg.getAddStatusHeader())
else code = cfg.getReturnTempErrorOnReject() ? "421" : "550"; code="250";
else
code=cfg.getReturnTempErrorOnReject()?"421":"550";
mechanism="dnsbl"; mechanism="dnsbl";
message=code+" You are listed on some DNS blacklists. Get delisted before trying to send us email."; message=code+" You are listed on some DNS blacklists. Get delisted before trying to send us email.";
std::cout << "checking " << mechanism << "\n"; LINF("checking " + mechanism);
} }
else if (cfg.getRejectNoReverseResolution() && !authenticated && "" == resolvedname) { else if(cfg.getRejectNoReverseResolution()&&!authenticated&&""==resolvedname)
{
code=cfg.getReturnTempErrorOnReject()?"421":"550"; code=cfg.getReturnTempErrorOnReject()?"421":"550";
mechanism="no reverse resolution"; mechanism="no reverse resolution";
message=code+" Your IP address does not resolve to a hostname."; message=code+" Your IP address does not resolve to a hostname.";
std::cout << "checking " << mechanism << "\n"; LINF("checking " + mechanism);
} }
else if (cfg.getCheckHeloAgainstReverse() && !authenticated && ehlostr != resolvedname) { else if(cfg.getCheckHeloAgainstReverse()&&!authenticated&&ehlostr!=resolvedname)
{
code=cfg.getReturnTempErrorOnReject()?"421":"550"; code=cfg.getReturnTempErrorOnReject()?"421":"550";
mechanism="helo differs from resolved name"; mechanism="helo differs from resolved name";
message=code+" Your IP hostname doesn't match your envelope hostname."; message=code+" Your IP hostname doesn't match your envelope hostname.";
std::cout << "checking " << mechanism << "\n"; LINF("checking " + mechanism);
} else {
code = "250";
} }
else
code="250";
if (!mechanism.empty()) strlog.insert(0, "(" + mechanism + ") "); if(""!=mechanism)
strlog.insert(0,"("+mechanism+") ");
strlog.insert(0,code+" "); strlog.insert(0,code+" ");
std::cout << strlog << "\n"; // Log the connection
// If we didn't accept the email, punish spammers //log the connection
if ("250" != code) { LINF(strlog);
//if we didn't accept the email, punish spammers
if("250"!=code)
{
inside.writeLine("QUIT"); inside.writeLine("QUIT");
inside.close(); // Close the socket now and leave server alone inside.close(); //close the socket now and leave server alone
std::this_thread::sleep_for(std::chrono::seconds(20)); sleep(20);
outside->writeLine(message); outside.writeLine(message);
return; return;
} }
last_state=SMTP_STATE_WAIT_FOR_DATA; last_state=SMTP_STATE_WAIT_FOR_DATA;
} }
// Handle STARTTLS if("starttls"==Utils::strtolower(strtemp.substr(0,8)))
if ("starttls" == Utils::strtolower(strtemp.substr(0, 8))) { {
//if we have ssl then accept starttls, if not politely say fuck you
#ifdef HAVE_SSL #ifdef HAVE_SSL
try { try
outside->prepareSSL(true); {
std::cout << "STARTTLS issued by remote, TLS enabled\n"; outside.prepareSSL(true);
outside->writeLine("220 You can speak now, line is secure!!"); LINF("STARTTLS issued by remote, TLS enabled");
outside->startSSL(true); outside.writeLine("220 You can speak now, line is secure!!");
} catch (Exception& e) { outside.startSSL(true);
std::cout << "STARTTLS issued by remote, but enableSSL failed!\n"; }
outside->writeLine("454 Tried to enable SSL but failed"); catch(Exception &e)
{
LINF("STARTTLS issued by remote, but enableSSL failed!");
LERR(e);
outside.writeLine("454 Tried to enable SSL but failed");
} }
#else #else
outside->writeLine("454 TLS temporarily not available"); outside.writeLine("454 TLS temporarily not available");
std::cout << "STARTTLS issued by remote, TLS was not enabled because this build lacks SSL support\n"; LINF("STARTTLS issued by remote, TLS was not enabled because this build lacks SSL support");
#endif //HAVE_SSL #endif //HAVE_SSL
strtemp=""; strtemp="";
} }
if (strtemp.length()) inside.writeLine(strtemp); if(strtemp.length())
inside.writeLine(strtemp);
} }
// Check if the server wants to send something to the client if(inside.canRead(0.2)) //server wants to send something to client
if (inside.canRead(0.2)) { {
strtemp=inside.readLine(); strtemp=inside.readLine();
if (inside.isClosed()) return; if(inside.isClosed())
return;
string code=strtemp.substr(0,3); //all responses by the server start with a code
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 and read binary-safe
if ("354" == code) { // 354 -> you can start sending data, unthrottle now {
std::string endofdata = ""; string endofdata="";
ssize_t bytes_read=0; ssize_t bytes_read=0;
char buffer[4097]; char buffer[4097];
outside->writeLine(strtemp);
do { outside.writeLine(strtemp);
bytes_read = outside->readBytes(buffer, sizeof(buffer) - 1); strtemp="";
if (bytes_read < 1) throw NetworkException("Problem reading DATA contents, recv returned " + Utils::inttostr(bytes_read), __FILE__, __LINE__); 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'; buffer[bytes_read]='\0';
inside.writeBytes(buffer,bytes_read); inside.writeBytes(buffer,bytes_read);
if (bytes_read < 5) endofdata += std::string(buffer); if(bytes_read<5)
else endofdata = std::string(buffer + bytes_read - 5); endofdata+=string(buffer);
if (endofdata.length() > 5) endofdata = endofdata.substr(endofdata.length() - 5); else
} while (endofdata != "\r\n.\r\n" && endofdata.length() > 3 && endofdata.substr(2) != "\n.\n" && endofdata.substr(2) != "\r.\r"); 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 if("235"==code) //235 -> you are correctly authenticated, unthrottle & authenticate
{
throttled=false; throttled=false;
authenticated=true; authenticated=true;
hermes_status="authenticated"; hermes_status="authenticated";
} }
if("250-pipelining"==Utils::strtolower(strtemp)||"250-chunking"==Utils::strtolower(strtemp)) //this solves our problems with pipelining-enabled servers
strtemp="";
// Try to annoy spammers who send too many senseless commands by delaying their connection //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)
if ("502" == code) { // 502 unimplemented //so we just say we support an imaginary extension (noextension).
if (cfg.getNumberOfUnimplementedCommandsAllowed() != -1 && ++unimplemented_requests > cfg.getNumberOfUnimplementedCommandsAllowed()) { //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.writeLine("QUIT");
inside.close(); // Close the socket now and leave server alone inside.close(); //close the socket now and leave server alone
std::this_thread::sleep_for(std::chrono::seconds(60)); sleep(60);
outside->writeLine("502 Too many unimplemented commands, closing connection"); outside.writeLine("502 Too many unimplemented commands, closing connection");
return; return;
} }
} }
if (strtemp.length()) outside->writeLine(strtemp); if(strtemp.length())
outside.writeLine(strtemp);
} }
if (throttled) std::this_thread::sleep_for(std::chrono::seconds(cfg.getThrottlingTime())); // Take 1 second between each command 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 }
std::cerr << "Exception occurred: " << e.what() << std::endl; 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; return;
} }
} }