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,16 +56,14 @@ 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 WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/docs)
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/docs) install(
install( 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

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