From d2a90e4cd1412df662f907a8a285d12e3826ef1c Mon Sep 17 00:00:00 2001 From: Juanjo Gutierrez Date: Sun, 23 Mar 2025 10:20:26 +0100 Subject: [PATCH 01/12] preliminar status, still need to make it work --- src/Proxy.cpp | 537 ++++++++++++++++++++------------------------------ 1 file changed, 216 insertions(+), 321 deletions(-) diff --git a/src/Proxy.cpp b/src/Proxy.cpp index 319417c..1515869 100644 --- a/src/Proxy.cpp +++ b/src/Proxy.cpp @@ -1,338 +1,233 @@ -/** - * 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 - */ +// Proxy.cpp #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 LOGGER_CLASS hermes_log; -extern Configfile cfg; +extern Configfile cfg; // External configuration -void Proxy::setOutside(Socket& p_outside) -{ - outside=p_outside; -} +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; -/** - * 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 + 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"; - 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); + // 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; } - catch(Exception &e) - { - LINF("STARTTLS issued by remote, but enableSSL failed!"); - LERR(e); - outside.writeLine("454 Tried to enable SSL but failed"); + } + + 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; + } } - #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); - } + // Connect to the inside server + inside.connect(cfg.getServerHost(), cfg.getServerPort()); - 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"*/); + #ifdef HAVE_SSL + if (cfg.getOutgoingSsl()) { + inside.prepareSSL(false); + inside.startSSL(false); } - - if("235"==code) //235 -> you are correctly authenticated, unthrottle & authenticate - { - throttled=false; - authenticated=true; - hermes_status="authenticated"; + if (cfg.getIncomingSsl()) { + outside->prepareSSL(true); + outside->startSSL(true); } - if("250-pipelining"==Utils::strtolower(strtemp)||"250-chunking"==Utils::strtolower(strtemp)) //this solves our problems with pipelining-enabled servers - strtemp=""; + #endif // HAVE_SSL - //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"; + // 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; - //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() > 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 } - - 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 + std::cerr << "Exception occurred: " << e.what() << std::endl; + return; } - } - catch(Exception &e) //any exception will close both connections - { - LERR(e); - if(last_state to "+(""==to?"no-to":to)); - return; - } } From 49f3fd3a6b6a595c475efe6f8c20b39a6d9c0567 Mon Sep 17 00:00:00 2001 From: Juanjo Gutierrez Date: Sun, 23 Mar 2025 10:27:53 +0100 Subject: [PATCH 02/12] some tests and more preliminary stuff --- .gitignore | 3 ++ src/Proxy.h | 69 +++++++---------------------------------- src/SocketInterface.cpp | 51 ++++++++++++++++++++++++++++++ src/SocketInterface.h | 16 ++++++++++ src/test/MockSocket.h | 15 +++++++++ src/test/ProxyTest.cpp | 63 +++++++++++++++++++++++++++++++++++++ 6 files changed, 159 insertions(+), 58 deletions(-) create mode 100644 .gitignore create mode 100644 src/SocketInterface.cpp create mode 100644 src/SocketInterface.h create mode 100644 src/test/MockSocket.h create mode 100644 src/test/ProxyTest.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a221c5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +docs/ +hermesrc.example diff --git a/src/Proxy.h b/src/Proxy.h index fd55da2..689aebf 100644 --- a/src/Proxy.h +++ b/src/Proxy.h @@ -1,61 +1,14 @@ -/** - * 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 - */ -#ifndef PROXY_H -#define PROXY_H +// Proxy.h +#pragma once +#include +#include "SocketInterface.h" -#include "hermes.h" -#include -#ifdef WIN32 - #include -#else - #include -#endif -#include -#include -#include -#include -#include -#include +class Proxy { +public: + Proxy(SocketInterface* outside_socket) : outside(outside_socket) {} + + void run(std::string& peer_address); -#include "Socket.h" -#include "Configfile.h" -#include "Utils.h" -#include "Logger.h" -#ifdef HAVE_SPF -#include "Spf.h" -#endif //HAVE_SPF - -#define SMTP_STATE_WAIT_FOR_HELO 0 -#define SMTP_STATE_WAIT_FOR_MAILFROM 1 -#define SMTP_STATE_WAIT_FOR_RCPTTO 2 -#define SMTP_STATE_WAIT_FOR_DATA 3 - -class Proxy -{ - private: - Socket outside; //connection from someone sending mail - Socket inside; //connection to our inside smtp - public: - //Proxy():outside(NULL),inside(NULL){}; - void setOutside(Socket&); - void run(string&); +private: + SocketInterface* outside; }; - -#endif //PROXY_H diff --git a/src/SocketInterface.cpp b/src/SocketInterface.cpp new file mode 100644 index 0000000..0eec521 --- /dev/null +++ b/src/SocketInterface.cpp @@ -0,0 +1,51 @@ +// BoostSocket.h +#include +#include "SocketInterface.h" + +class BoostSocket : public SocketInterface { +public: + BoostSocket() : socket_(io_service_) {} + + void connect(const std::string& host, unsigned short port) override { + boost::asio::ip::tcp::resolver resolver(io_service_); + boost::asio::ip::tcp::resolver::results_type endpoints = resolver.resolve(host, std::to_string(port)); + boost::asio::connect(socket_, endpoints); + } + + void writeLine(const std::string& data) override { + boost::asio::write(socket_, boost::asio::buffer(data + "\r\n")); + } + + std::string readLine() override { + boost::asio::streambuf buf; + boost::asio::read_until(socket_, buf, "\r\n"); + std::istream is(&buf); + std::string line; + std::getline(is, line); + return line; + } + + bool canRead(double timeout) override { + // Implementation to check if data is available to read. + } + + bool isClosed() override { + return !socket_.is_open(); + } + + void close() override { + socket_.close(); + } + + void prepareSSL(bool incoming) override { + // Implement SSL preparation here if needed. + } + + void startSSL(bool incoming) override { + // Implement starting SSL here if needed. + } + +private: + boost::asio::io_service io_service_; + boost::asio::ip::tcp::socket socket_; +}; diff --git a/src/SocketInterface.h b/src/SocketInterface.h new file mode 100644 index 0000000..bce1566 --- /dev/null +++ b/src/SocketInterface.h @@ -0,0 +1,16 @@ +// SocketInterface.h +#pragma once +#include + +class SocketInterface { +public: + virtual ~SocketInterface() = default; + virtual void connect(const std::string& host, unsigned short port) = 0; + virtual void writeLine(const std::string& data) = 0; + virtual std::string readLine() = 0; + virtual bool canRead(double timeout) = 0; + virtual bool isClosed() = 0; + virtual void close() = 0; + virtual void prepareSSL(bool incoming) = 0; + virtual void startSSL(bool incoming) = 0; +}; diff --git a/src/test/MockSocket.h b/src/test/MockSocket.h new file mode 100644 index 0000000..63e3819 --- /dev/null +++ b/src/test/MockSocket.h @@ -0,0 +1,15 @@ +// MockSocket.h +#include "SocketInterface.h" +#include + +class MockSocket : public SocketInterface { +public: + MOCK_METHOD(void, connect, (const std::string& host, unsigned short port), (override)); + MOCK_METHOD(void, writeLine, (const std::string& data), (override)); + MOCK_METHOD(std::string, readLine, (), (override)); + MOCK_METHOD(bool, canRead, (double timeout), (override)); + MOCK_METHOD(bool, isClosed, (), (override)); + MOCK_METHOD(void, close, (), (override)); + MOCK_METHOD(void, prepareSSL, (bool incoming), (override)); + MOCK_METHOD(void, startSSL, (bool incoming), (override)); +}; diff --git a/src/test/ProxyTest.cpp b/src/test/ProxyTest.cpp new file mode 100644 index 0000000..c8e0412 --- /dev/null +++ b/src/test/ProxyTest.cpp @@ -0,0 +1,63 @@ +// ProxyTest.cpp +#include +#include "Proxy.h" +#include "MockSocket.h" + +class ProxyTest : public ::testing::Test { +protected: + MockSocket mock_socket; + Proxy* proxy; + + void SetUp() override { + proxy = new Proxy(&mock_socket); + } + + void TearDown() override { + delete proxy; + } +}; + +TEST_F(ProxyTest, HandlesMailFromCommand) { + std::string peer_address = "127.0.0.1"; + + EXPECT_CALL(mock_socket, connect("server-host", 25)); + EXPECT_CALL(mock_socket, canRead(0.2)).WillOnce(testing::Return(true)); + EXPECT_CALL(mock_socket, readLine()).WillOnce(testing::Return("MAIL FROM:")); + EXPECT_CALL(mock_socket, writeLine("MAIL FROM:")); + EXPECT_CALL(mock_socket, writeLine("250 OK")); + + proxy->run(peer_address); +} + +TEST_F(ProxyTest, HandlesRcptToCommand) { + std::string peer_address = "127.0.0.1"; + + EXPECT_CALL(mock_socket, connect("server-host", 25)); + EXPECT_CALL(mock_socket, canRead(0.2)).WillRepeatedly(testing::Return(true)); + EXPECT_CALL(mock_socket, readLine()) + .WillOnce(testing::Return("MAIL FROM:")) + .WillOnce(testing::Return("RCPT TO:")) + .WillOnce(testing::Return("")); + + EXPECT_CALL(mock_socket, writeLine("MAIL FROM:")); + EXPECT_CALL(mock_socket, writeLine("RCPT TO:")); + EXPECT_CALL(mock_socket, writeLine("250 OK")); + + proxy->run(peer_address); +} + +TEST_F(ProxyTest, HandlesEmptyLine) { + std::string peer_address = "127.0.0.1"; + + EXPECT_CALL(mock_socket, connect("server-host", 25)); + EXPECT_CALL(mock_socket, canRead(0.2)).WillOnce(testing::Return(true)); + EXPECT_CALL(mock_socket, readLine()).WillOnce(testing::Return("")); + + proxy->run(peer_address); + // Expect no further actions to occur +} + +int main(int argc, char** argv) { + ::testing::InitGoogleMock(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file From 518f5782981dce14ba1d860fcb2d001230299b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Guti=C3=A9rrez=20de=20Quevedo=20P=C3=A9?= =?UTF-8?q?rez?= Date: Tue, 25 Mar 2025 10:42:06 +0100 Subject: [PATCH 03/12] Refactor Spf --- src/Spf.cpp | 121 +++++++++--------- src/Spf.h | 2 +- src/test/mocks/libspf2_mock.h | 96 ++++++++++++++ .../{MockSocket.h => mocks/socket_mock.h} | 0 .../{ProxyTest.cpp => tests/proxy_test.cpp} | 4 +- src/test/tests/spf_test.cpp | 98 ++++++++++++++ 6 files changed, 261 insertions(+), 60 deletions(-) create mode 100644 src/test/mocks/libspf2_mock.h rename src/test/{MockSocket.h => mocks/socket_mock.h} (100%) rename src/test/{ProxyTest.cpp => tests/proxy_test.cpp} (98%) create mode 100644 src/test/tests/spf_test.cpp diff --git a/src/Spf.cpp b/src/Spf.cpp index 1ef0bf2..0eeec44 100644 --- a/src/Spf.cpp +++ b/src/Spf.cpp @@ -4,11 +4,11 @@ * * 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 + * 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 + * 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 @@ -17,83 +17,90 @@ * * @author Juan José Gutiérrez de Quevedo */ + #include "Spf.h" +#include +#include +#include -SPF_server_t *Spf::spfserver=NULL; +SPF_server_t *Spf::spfserver = nullptr; /** - * constructor - * - * it will create a spfserver if this is the first created object of the class. - * if it isn't, then we just create an spfrequest + * Constructor + * + * Initializes the SPF server if this is the first created object of the class. */ -Spf::Spf():spfrequest(NULL),spfresponse(NULL) -{ - pthread_mutex_init(&mutex,NULL); - if(NULL==spfserver) - if(NULL==(spfserver=SPF_server_new(SPF_DNS_CACHE,0))) - throw Exception(_("Can't initialize SPF library"),__FILE__,__LINE__); - - if(NULL==(spfrequest=SPF_request_new(spfserver))) - throw Exception(_("Can't initialize SPF request"),__FILE__,__LINE__); +Spf::Spf() { + static std::once_flag initFlag; // To ensure thread-safe initialization + std::call_once(initFlag, []() { + spfserver = SPF_server_new(SPF_DNS_CACHE, 0); + if (!spfserver) { + throw std::runtime_error("Can't initialize SPF library"); + } + }); } /** - * destructor - * - * frees the memory of the spfrequest + * Destructor + * + * Frees the memory of the SPF server. */ -Spf::~Spf() -{ - pthread_mutex_destroy(&mutex); - if(NULL!=spfrequest) SPF_request_free(spfrequest); +Spf::~Spf() { + deinitialize(); // Clean up resources } /** - * frees all memory related to the spf class + * Frees all memory related to the SPF class. * - * this is needed because the common things are only initialized - * once (and are static), and when we close the program we need - * to deinitialize them + * This is needed because common resources are only initialized once (and are static). */ -void Spf::deinitialize() -{ - if(NULL!=spfserver) - SPF_server_free(spfserver); +void Spf::deinitialize() { + if (spfserver) { + SPF_server_free(spfserver); + spfserver = nullptr; // Optional: Avoid dangling pointer + } } /** - * make a query to the dns system for an spf record + * Makes a query to the DNS system for an SPF record. * - * highly inspired from fakehermes' source + * @param ip The IP of the remote server + * @param helo The HELO string of the remote server + * @param from The envelope from address * - * @param ip the ip of the remote server - * @param helo the hello string of the remote server - * @param from the envelope from address - * - * @returns true if it is not incorrect + * @returns true if the query is not incorrect */ -bool Spf::query(string ip,string helo,string from) -{ - bool retval=false; +bool Spf::query(const std::string& ip, const std::string& helo, const std::string& from) { + SPF_request_t* spfrequest = SPF_request_new(spfserver); // Create request here + if (!spfrequest) { + throw std::runtime_error("Can't initialize SPF request"); + } - if(SPF_request_set_ipv4_str(spfrequest,ip.c_str())) - throw Exception(_("Error configuring IP for SPF request"),__FILE__,__LINE__); - if(SPF_request_set_helo_dom(spfrequest,helo.c_str())) - throw Exception(_("Error configuring HELO for SPF request"),__FILE__,__LINE__); - if(SPF_request_set_env_from(spfrequest,from.c_str())) - throw Exception(_("Error configuring FROM for SPF request"),__FILE__,__LINE__); + // Set the values for the SPF request + if (SPF_request_set_ipv4_str(spfrequest, ip.c_str())) { + SPF_request_free(spfrequest); // Clean up on failure + throw std::runtime_error("Error configuring IP for SPF request"); + } + if (SPF_request_set_helo_dom(spfrequest, helo.c_str())) { + SPF_request_free(spfrequest); // Clean up on failure + throw std::runtime_error("Error configuring HELO for SPF request"); + } + if (SPF_request_set_env_from(spfrequest, from.c_str())) { + SPF_request_free(spfrequest); // Clean up on failure + throw std::runtime_error("Error configuring FROM for SPF request"); + } - //make the actual query - pthread_mutex_lock(&mutex); - SPF_request_query_mailfrom(spfrequest,&spfresponse); - pthread_mutex_unlock(&mutex); + // Make the actual query + SPF_response_t* spfresponse = nullptr; // Local response variable + SPF_request_query_mailfrom(spfrequest, &spfresponse); - if(NULL!=spfresponse) - { - retval=(SPF_RESULT_FAIL==SPF_response_result(spfresponse)||SPF_RESULT_SOFTFAIL==SPF_response_result(spfresponse))?false:true; - SPF_response_free(spfresponse); - } + bool retval = false; + if (spfresponse) { + retval = !(SPF_response_result(spfresponse) == SPF_RESULT_FAIL || + SPF_response_result(spfresponse) == SPF_RESULT_SOFTFAIL); + SPF_response_free(spfresponse); // Free the response + } - return retval; + SPF_request_free(spfrequest); // Free the request + return retval; } diff --git a/src/Spf.h b/src/Spf.h index 9e1a07f..ce968fd 100644 --- a/src/Spf.h +++ b/src/Spf.h @@ -38,7 +38,7 @@ class Spf Spf(); ~Spf(); static void deinitialize(); - bool query(string,string,string); + bool query(const string&, const string&, const string&); }; #endif //SPF_H diff --git a/src/test/mocks/libspf2_mock.h b/src/test/mocks/libspf2_mock.h new file mode 100644 index 0000000..09d61b9 --- /dev/null +++ b/src/test/mocks/libspf2_mock.h @@ -0,0 +1,96 @@ +#ifndef LIBSPF2_MOCK_H +#define LIBSPF2_MOCK_H +#include +#include +#include +#include + +// Control variables to adjust behavior of mocked functions +extern "C" { + static SPF_errcode_t spf_set_ipv4_result = SPF_E_SUCCESS; + static SPF_errcode_t spf_set_helo_result = SPF_E_SUCCESS; + static int spf_set_from_result; + static SPF_response_t* spf_mock_response = nullptr; + static SPF_errcode_t spf_request_query_result = SPF_E_SUCCESS; + + // New control variables for SPF_server mocking + static SPF_server_t* spf_mock_server = nullptr; + static bool spf_server_new_should_fail = false; + + SPF_server_t* SPF_server_new(SPF_server_dnstype_t dnstype,int debug) + { + if (spf_server_new_should_fail) { + return nullptr; + } + return spf_mock_server ? spf_mock_server : new SPF_server_t(); + } + + void SPF_server_free(SPF_server_t* server) { + delete server; + } + + SPF_request_t* SPF_request_new(SPF_server_t *server) { + return new SPF_request_t(); // Simply allocate and return new request + } + + void SPF_request_free(SPF_request_t* request) { + delete request; // Deallocate request + } + + SPF_errcode_t SPF_request_set_ipv4_str(SPF_request_t* request, const char* ip) { + return spf_set_ipv4_result; // Use controllable result + } + + SPF_errcode_t SPF_request_set_helo_dom(SPF_request_t* request, const char* helo) { + return spf_set_helo_result; // Use controllable result + } + + int SPF_request_set_env_from(SPF_request_t* request, const char* from) { + return spf_set_from_result; // Use controllable result + } + + SPF_errcode_t SPF_request_query_mailfrom(SPF_request_t* request, SPF_response_t** response) { + *response = spf_mock_response; + return spf_request_query_result; + } + + SPF_result_t SPF_response_result(SPF_response_t* response) { + return response->result; // Return the result + } + + void SPF_response_free(SPF_response_t* response) { + delete response; // Deallocate response + } +} + +// Functions to set return values for mocked functions +void SetSpfMockReturnIPv4Value(SPF_errcode_t value) { + spf_set_ipv4_result = value; +} + +void SetSpfMockReturnHeloValue(SPF_errcode_t value) { + spf_set_helo_result = value; +} + +void SetSpfMockReturnFromValue(int value) { + spf_set_from_result = value; +} + +void SetSpfRequestQueryResult(SPF_errcode_t value) { + spf_request_query_result = value; +} + +void SetSpfMockResponse(SPF_response_t* mockResponse) { + spf_mock_response = mockResponse; +} + +// New functions for SPF_server mocking +void SetSpfMockServer(SPF_server_t* mockServer) { + spf_mock_server = mockServer; +} + +void SetSpfServerNewShouldFail(bool shouldFail) { + spf_server_new_should_fail = shouldFail; +} + +#endif // LIBSPF2_MOCK_H diff --git a/src/test/MockSocket.h b/src/test/mocks/socket_mock.h similarity index 100% rename from src/test/MockSocket.h rename to src/test/mocks/socket_mock.h diff --git a/src/test/ProxyTest.cpp b/src/test/tests/proxy_test.cpp similarity index 98% rename from src/test/ProxyTest.cpp rename to src/test/tests/proxy_test.cpp index c8e0412..7a06603 100644 --- a/src/test/ProxyTest.cpp +++ b/src/test/tests/proxy_test.cpp @@ -1,7 +1,7 @@ // ProxyTest.cpp #include #include "Proxy.h" -#include "MockSocket.h" +#include "socket_mock.h" class ProxyTest : public ::testing::Test { protected: @@ -60,4 +60,4 @@ TEST_F(ProxyTest, HandlesEmptyLine) { int main(int argc, char** argv) { ::testing::InitGoogleMock(&argc, argv); return RUN_ALL_TESTS(); -} \ No newline at end of file +} diff --git a/src/test/tests/spf_test.cpp b/src/test/tests/spf_test.cpp new file mode 100644 index 0000000..b205875 --- /dev/null +++ b/src/test/tests/spf_test.cpp @@ -0,0 +1,98 @@ +#include +#include "Spf.h" + +#include "libspf2_mock.h" + +// Mock the SPF server responses for unit testing +class MockSPFServer { +public: + static void initialize() { + // Initialize any mock data here + } + + static void cleanup() { + // Cleanup if needed + } + + static SPF_response_t* mockResponse(SPF_result_t result) { + SPF_response_t* response = new SPF_response_t; // Replace with actual allocation + response->result = result; // Set the desired mock result + return response; + } +}; + +// Test Fixture for SPF tests +class SpfTest : public ::testing::Test { +protected: + Spf* spf; + + void SetUp() override { + // Create a new Spf instance before each test + spf = new Spf(); + } + + void TearDown() override { + // Clean up after each test + delete spf; + spf->deinitialize(); + } +}; + +// Test the construction of the Spf object +TEST_F(SpfTest, Construction) { + EXPECT_NO_THROW({ + Spf testSpf; + }); +} + +// Test SPF querying with valid parameters +TEST_F(SpfTest, QueryValidParameters) { + // Assuming the mocked SPF server is set up to return a valid response + MockSPFServer::initialize(); + + bool result = spf->query("192.0.2.1", "mail.example.com", "test@example.com"); + EXPECT_TRUE(result); + + MockSPFServer::cleanup(); +} + +// Test SPF querying with failed result +TEST_F(SpfTest, QueryFailResult) { + // Mock the response to return a fail result here + SPF_response_t* response = MockSPFServer::mockResponse(SPF_RESULT_FAIL); + + bool result = spf->query("198.51.100.1", "fail.example.com", "test@fail.com"); + EXPECT_FALSE(result); + + delete response; // Clean up mocked response +} + +// Test SPF querying with an empty HELO string +TEST_F(SpfTest, QueryEmptyHelo) { + EXPECT_THROW({ + spf->query("192.0.2.1", "", "test@example.com"); + }, std::runtime_error); +} + +// Test SPF querying with invalid IP format +TEST_F(SpfTest, QueryInvalidIPAddress) { + EXPECT_THROW({ + spf->query("invalid_ip", "mail.example.com", "test@example.com"); + }, std::runtime_error); +} + +// Test SPF request failure +TEST_F(SpfTest, QueryRequestFailure) { + // Here you might want to simulate a situation + // where the request cannot be created or fails due to some other reason. + spf->deinitialize(); // Ensure the server is cleaned up before running this. + + EXPECT_THROW({ + spf->query("192.0.2.1", "mail.example.com", "test@example.com"); + }, std::runtime_error); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From d7e2aee3a35b2baa9501d783a46131289a9844d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Guti=C3=A9rrez=20de=20Quevedo=20P=C3=A9?= =?UTF-8?q?rez?= Date: Tue, 25 Mar 2025 10:59:11 +0100 Subject: [PATCH 04/12] Rearrange stuff --- CMakeLists.txt | 1 + dists/fc_init | 62 -------------------- dists/hermes.spec.in | 77 ------------------------- {src => include}/Database.h | 0 {src => include}/Exception.h | 0 {src => include}/FileLogger.h | 0 {src => include}/Logger.h | 0 {src => include}/NullLogger.h | 0 {src => include}/Proxy.h | 5 ++ {src => include}/ServerSocket.h | 0 {src => include}/Socket.h | 0 {src => include}/SocketInterface.h | 0 {src => include}/Spf.h | 0 {src => include}/UnixLogger.h | 0 {src => include}/Utils.h | 0 {src => include}/hermes.h | 0 test/CMakeLists.txt | 14 +++++ {src/test => test}/mocks/libspf2_mock.h | 0 {src/test => test}/mocks/socket_mock.h | 0 {src/test => test}/tests/proxy_test.cpp | 0 {src/test => test}/tests/spf_test.cpp | 0 21 files changed, 20 insertions(+), 139 deletions(-) delete mode 100755 dists/fc_init delete mode 100644 dists/hermes.spec.in rename {src => include}/Database.h (100%) rename {src => include}/Exception.h (100%) rename {src => include}/FileLogger.h (100%) rename {src => include}/Logger.h (100%) rename {src => include}/NullLogger.h (100%) rename {src => include}/Proxy.h (63%) rename {src => include}/ServerSocket.h (100%) rename {src => include}/Socket.h (100%) rename {src => include}/SocketInterface.h (100%) rename {src => include}/Spf.h (100%) rename {src => include}/UnixLogger.h (100%) rename {src => include}/Utils.h (100%) rename {src => include}/hermes.h (100%) create mode 100644 test/CMakeLists.txt rename {src/test => test}/mocks/libspf2_mock.h (100%) rename {src/test => test}/mocks/socket_mock.h (100%) rename {src/test => test}/tests/proxy_test.cpp (100%) rename {src/test => test}/tests/spf_test.cpp (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index e25dd14..652d7fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ if(WIN32) target_compile_definitions(hermes PRIVATE WIN32) endif() +target_include_directories(hermes PRIVATE include) target_compile_definitions(hermes PRIVATE LOGGER_CLASS=${LOGGER_CLASS}) target_sources(hermes PRIVATE src/${LOGGER_CLASS}.cpp) diff --git a/dists/fc_init b/dists/fc_init deleted file mode 100755 index f4f30a4..0000000 --- a/dists/fc_init +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/bash -# Startup script for hermes -# -# chkconfig: 3 95 05 -# description: hermes - -# Source function library. -. /etc/rc.d/init.d/functions - -prog=hermes -configfile=/etc/hermes/hermesrc - -start() { - echo -n $"Starting $prog: " - daemon --check=$prog /usr/bin/hermes $configfile - RETVAL=$? - echo -} - -stop() { - echo -n $"Stopping $prog: " - killproc $prog - RETVAL=$? - echo -} - -safestop() { - echo -n $"Stopping $prog(will process pending connections):" - killproc $prog -INT - rm /var/run/$prog.pid - RETVAL=$? - echo -} - -case "$1" in - start) - start - ;; - - stop) - stop - ;; - - restart) - safestop - sleep 2 - start - ;; - condrestart) - if test "x`pidfileofproc $prog`" != x; then - stop - start - fi - ;; - - *) - echo $"Usage: $0 {start|stop|restart|condrestart}" - exit 1 - -esac - -exit $RETVAL diff --git a/dists/hermes.spec.in b/dists/hermes.spec.in deleted file mode 100644 index c69296c..0000000 --- a/dists/hermes.spec.in +++ /dev/null @@ -1,77 +0,0 @@ -Summary: An anti-spam SMTP proxy -Name: @PACKAGE@ -Version: @VERSION@ -Release: 0 -License: GPL -Group: System Environment/Daemons -Packager: Veit Wahlich -URL: http://www.hermes-project.com/ -Source0: http://www.hermes-project.com/files/%{name}-%{version}.tar.bz2 -Buildroot: %{_tmppath}/%{name}-%{version}-%{release}-root - -%description -hermes is a generic, lightweight, portable and fast anti-spam smtp proxy. -Supports greylisting, dns blacklisting/whitelisting, protocol throttling, banner delaying, spf and some -other tricks to reject most spam before it even enters your system. - -%prep -%setup -q - -%build -%configure --docdir=%{_datadir}/doc/%{name}-%{version} -%__make %{?_smp_mflags} - -%install -%__rm -rf %{buildroot} -%__make DESTDIR=%{buildroot} install -%__mkdir_p %{buildroot}%{_sysconfdir}/rc.d/init.d -%__mkdir_p %{buildroot}%{_sysconfdir}/hermes -%__mkdir_p %{buildroot}%{_localstatedir}/hermes -%__install -m 0755 dists/fc_init %{buildroot}%{_sysconfdir}/rc.d/init.d/hermes -%__install -m 0600 dists/hermesrc.example %{buildroot}%{_sysconfdir}/hermes/hermesrc - -%clean -%__rm -rf %{buildroot} - -%post -/sbin/chkconfig --add hermes - -%preun -if [ $1 = 0 ]; then # execute this only if we are NOT doing an upgrade - %{_sysconfdir}/rc.d/init.d/hermes stop >/dev/null 2>&1 - /sbin/chkconfig --del hermes -fi -exit 0 - -%files -%defattr(-, root, root, 0755) -%doc ChangeLog TODO AUTHORS dists/hermesrc.example docs/hermes-options.html docs/installing-hermes.txt docs/gpl.txt -%{_bindir}/hermes -%{_sysconfdir}/rc.d/init.d/hermes -%config %{_sysconfdir}/hermes/hermesrc -%dir %attr(0700,nobody,nobody) %{_localstatedir}/hermes - -%changelog -* Thu Jun 14 2007 Juan José Gutiérrez de Quevedo 1.4 -- removed patches, they are now on upstream - -* Fri May 25 2007 Veit Wahlich 1.3-2 -- added patch fix_whether (documentation fixes) -- added patch add_rejectnoresolve (reject on no DNS reverse resolution feature) -- changed RPM group to system daemon standard - -* Sat May 19 2007 Veit Wahlich 1.3-1 -- Made /etc/hermes/hermesrc readonly as it may contain passwords -- Fixed ownership and permissions of /var/hermes to match configuration default -- Silenced setup macro output as required by some distributions -- Fixed docdir to a LSB compliant location, will be replaced by rpmbuild -- Packaged extra documentation -- Removed hermes-options.html.in from docs -- Use directory macros for files section -- Further specfile cleanups and macro usage - -* Tue May 15 2007 Juan José Gutiérrez de Quevedo -- Fixed rpm to create /var/hermes - -* Fri Apr 11 2007 Juan José Gutiérrez de Quevedo -- Initial release diff --git a/src/Database.h b/include/Database.h similarity index 100% rename from src/Database.h rename to include/Database.h diff --git a/src/Exception.h b/include/Exception.h similarity index 100% rename from src/Exception.h rename to include/Exception.h diff --git a/src/FileLogger.h b/include/FileLogger.h similarity index 100% rename from src/FileLogger.h rename to include/FileLogger.h diff --git a/src/Logger.h b/include/Logger.h similarity index 100% rename from src/Logger.h rename to include/Logger.h diff --git a/src/NullLogger.h b/include/NullLogger.h similarity index 100% rename from src/NullLogger.h rename to include/NullLogger.h diff --git a/src/Proxy.h b/include/Proxy.h similarity index 63% rename from src/Proxy.h rename to include/Proxy.h index 689aebf..7e266c0 100644 --- a/src/Proxy.h +++ b/include/Proxy.h @@ -3,6 +3,11 @@ #include #include "SocketInterface.h" +#define SMTP_STATE_WAIT_FOR_HELO 0 +#define SMTP_STATE_WAIT_FOR_MAILFROM 1 +#define SMTP_STATE_WAIT_FOR_RCPTTO 2 +#define SMTP_STATE_WAIT_FOR_DATA 3 + class Proxy { public: Proxy(SocketInterface* outside_socket) : outside(outside_socket) {} diff --git a/src/ServerSocket.h b/include/ServerSocket.h similarity index 100% rename from src/ServerSocket.h rename to include/ServerSocket.h diff --git a/src/Socket.h b/include/Socket.h similarity index 100% rename from src/Socket.h rename to include/Socket.h diff --git a/src/SocketInterface.h b/include/SocketInterface.h similarity index 100% rename from src/SocketInterface.h rename to include/SocketInterface.h diff --git a/src/Spf.h b/include/Spf.h similarity index 100% rename from src/Spf.h rename to include/Spf.h diff --git a/src/UnixLogger.h b/include/UnixLogger.h similarity index 100% rename from src/UnixLogger.h rename to include/UnixLogger.h diff --git a/src/Utils.h b/include/Utils.h similarity index 100% rename from src/Utils.h rename to include/Utils.h diff --git a/src/hermes.h b/include/hermes.h similarity index 100% rename from src/hermes.h rename to include/hermes.h diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..4f0c645 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.31) + +project(unittests) + +find_package(GTest REQUIRED) +include_directories(${GTEST_INCLUDE_DIRS}) + +add_executable(spf_test tests/spf_test.cpp ../src/Spf.cpp) # Your test file +target_include_directories(spf_test PRIVATE ../include mocks) +target_link_libraries(spf_test ${GTEST_LIBRARIES} pthread) + +add_executable(proxy_test tests/proxy_test.cpp ../src/Proxy.cpp) # Your test file +target_include_directories(proxy_test PRIVATE ../include mocks) +target_link_libraries(proxy_test ${GTEST_LIBRARIES} pthread) diff --git a/src/test/mocks/libspf2_mock.h b/test/mocks/libspf2_mock.h similarity index 100% rename from src/test/mocks/libspf2_mock.h rename to test/mocks/libspf2_mock.h diff --git a/src/test/mocks/socket_mock.h b/test/mocks/socket_mock.h similarity index 100% rename from src/test/mocks/socket_mock.h rename to test/mocks/socket_mock.h diff --git a/src/test/tests/proxy_test.cpp b/test/tests/proxy_test.cpp similarity index 100% rename from src/test/tests/proxy_test.cpp rename to test/tests/proxy_test.cpp diff --git a/src/test/tests/spf_test.cpp b/test/tests/spf_test.cpp similarity index 100% rename from src/test/tests/spf_test.cpp rename to test/tests/spf_test.cpp From b05c4ad5d99d9a69809cab26080c81e5c8273901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Guti=C3=A9rrez=20de=20Quevedo=20P=C3=A9?= =?UTF-8?q?rez?= Date: Wed, 26 Mar 2025 13:49:59 +0100 Subject: [PATCH 05/12] Update config generation --- CMakeLists.txt | 48 ++++++---- {src => include}/Configfile.h.in | 0 scripts/generate_config.py | 147 ++++++++++++++++++++----------- src/Configfile.tmpl | 64 -------------- 4 files changed, 129 insertions(+), 130 deletions(-) rename {src => include}/Configfile.h.in (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 652d7fa..272d3ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,7 @@ set(LOGGER_CLASS UnixLogger CACHE STRING "One of UnixLogger, FileLogger or NullL add_executable (hermes ${CMAKE_CURRENT_BINARY_DIR}/Configfile.cpp + ${CMAKE_CURRENT_BINARY_DIR}/Configfile.h src/Exception.cpp src/hermes.cpp src/ServerSocket.cpp @@ -14,7 +15,7 @@ add_executable (hermes src/Proxy.cpp src/Socket.cpp) -option(BUILD_DOC "Build documentation") +option(BUILD_DOCS "Build documentation") if(WIN32) set(SOURCES ${SOURCES} @@ -31,7 +32,7 @@ target_sources(hermes PRIVATE src/${LOGGER_CLASS}.cpp) find_library (SQLITE3_LIBRARY NAMES libsqlite3 sqlite3) # optional dependency libspf2 -find_library (SPF2_LIBRARY NAMES spf2 libspf2) +find_library (SPF2_LIBRARY REQUIRED NAMES spf2 libspf2) if(SPF2_LIB) target_compile_definitions(hermes PRIVATE HAVE_SPF2) target_sources(hermes PRIVATE src/Spf.cpp) @@ -49,26 +50,43 @@ include_directories( ${CMAKE_CURRENT_BINARY_DIR} src) +set(CONFIG_TEMPLATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Configfile.tmpl) + # generation of various files add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Configfile.cpp - COMMAND cpp ${OPT_DEFS} ${CMAKE_CURRENT_SOURCE_DIR}/src/Configfile.tmpl -I - ${CMAKE_CURRENT_BINARY_DIR} | - python ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_config.py - DEPENDS src/Configfile.cpp.in src/Configfile.h.in src/Configfile.tmpl - docs/hermes-options.html.in scripts/generate_config.py) + COMMAND python "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_config.py" "${CONFIG_TEMPLATE}" + --cpp-template "${CMAKE_CURRENT_SOURCE_DIR}/src/Configfile.cpp.in" + --output-cpp "${CMAKE_CURRENT_BINARY_DIR}/Configfile.cpp" + DEPENDS ${CONFIG_TEMPLATE} src/Configfile.cpp.in) +add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Configfile.h + COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_config.py "${CONFIG_TEMPLATE}" + --h-template "${CMAKE_CURRENT_SOURCE_DIR}/include/Configfile.h.in" + --output-h "${CMAKE_CURRENT_BINARY_DIR}/Configfile.h" + DEPENDS ${CONFIG_TEMPLATE} include/Configfile.h.in) + +# DEPENDS src/Configfile.cpp.in src/Configfile.h.in src/Configfile.tmpl +# docs/hermes-options.html.in scripts/generate_config.py) # doxygen if (BUILD_DOCS) + add_custom_command(OUTPUT docs/hermesrc.example + COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_config.py "${CONFIG_TEMPLATE}" + --output-example "${CMAKE_CURRENT_BINARY_DIR}/docs/hermesrc.example" + DEPENDS ${CONFIG_TEMPLATE}) + add_custom_command(OUTPUT docs/html/hermes-options.html + COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_config.py "${CONFIG_TEMPLATE}" + --html-template "${CMAKE_CURRENT_SOURCE_DIR}/docs/hermes-options.html.in" + --output-html "${CMAKE_CURRENT_BINARY_DIR}/docs/html/hermes-options.html" + DEPENDS ${CONFIG_TEMPLATE} docs/hermes-options.html.in) 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() + add_custom_target(doc ALL + doxygen "${CMAKE_CURRENT_SOURCE_DIR}/docs/Doxyfile" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/docs + DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/docs/html/hermes-options.html" "${CMAKE_CURRENT_BINARY_DIR}/docs/hermesrc.example") + install( + DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/docs/html + TYPE DOC) endif() target_link_libraries(hermes diff --git a/src/Configfile.h.in b/include/Configfile.h.in similarity index 100% rename from src/Configfile.h.in rename to include/Configfile.h.in diff --git a/scripts/generate_config.py b/scripts/generate_config.py index fff5597..b864250 100644 --- a/scripts/generate_config.py +++ b/scripts/generate_config.py @@ -1,37 +1,63 @@ #!/usr/bin/env python3 - import sys -import re import string +import argparse +import os def camel_case(str_): """Convert snake_case to CamelCase.""" return string.capwords(str_, "_").replace("_", "") def main(): - # Read input files - with open('../docs/hermes-options.html.in', 'r') as f: - html_templ = f.read() + # Set up argument parser + parser = argparse.ArgumentParser(description='Generate configuration files for Hermes.') + parser.add_argument('input_template', + type=argparse.FileType('r'), + help='Input configuration template file') + parser.add_argument('--html-template', + default='', + help='Path to HTML template input file') + parser.add_argument('--cpp-template', + default='', + help='Path to C++ template input file') + parser.add_argument('--h-template', + default='', + help='Path to header template input file') + parser.add_argument('--output-cpp', + default='', + help='Output path for generated C++ file') + parser.add_argument('--output-h', + default='', + help='Output path for generated header file') + parser.add_argument('--output-example', + default='', + help='Output path for example configuration') + parser.add_argument('--output-html', + default='', + help='Output path for generated HTML documentation') - hvar1 = "" - hvar2 = "" - cppvar1 = "" - cppvar2 = "" - cppvar3 = "" - conf_example = "" - htmlvar = "" + # Parse arguments + args = parser.parse_args() + + hvar1 = [] + hvar2 = [] + cppvar1 = [] + cppvar2 = [] + cppvar3 = [] + conf_example = [] + htmlvar = [] htmlexpl = "" # Process input - for line in sys.stdin: + for line in args.input_template: line = line.strip() if not line or line.startswith('#') or line.startswith('*'): if line == '*clean*': htmlexpl = "" elif line.startswith('*'): - line = line.replace('*', '#') - conf_example += line + "\n" + line = line.replace('*', '#').strip() + conf_example.append(line) # Convert line for HTML line_html = line.lstrip('#').replace('>', '>') @@ -52,61 +78,80 @@ def main(): camel_name = camel_case(var_name) # Generate header variables - hvar1 += f"{type_str} {var_name};\n" - hvar2 += f"{type_str}& get{camel_name}();\n" + hvar1.append(f"{type_str} {var_name};") + hvar2.append(f"{type_str}& get{camel_name}();") # Generate cpp variables if 'list' in type_str: - cppvar1 += f"{var_name} = Configfile::parseAsList({default_val});\n" + cppvar1.append(f"{var_name} = Configfile::parseAsList({default_val});") else: - cppvar1 += f"{var_name} = {default_val};\n" + cppvar1.append(f"{var_name} = {default_val};") - cppvar2 += f"PARSE_{parts[0].upper()}(\"{var_name}\", {var_name})\n" - cppvar3 += f"GET_VAR(get{camel_name}, {var_name}, {type_str}&)\n" + cppvar2.append(f"PARSE_{parts[0].upper()}(\"{var_name}\", {var_name})") + cppvar3.append(f"GET_VAR(get{camel_name}, {var_name}, {type_str}&)") # Generate config example - conf_example += f"{var_name} = {default_val}\n\n" + conf_example.append(f"{var_name} = {default_val}") # Generate HTML - html_temp = html_templ.replace('%type%', parts[0]) \ - .replace('%name%', var_name) \ - .replace('%default%', default_val) \ - .replace('%explanation%', htmlexpl) - htmlvar += html_temp + if args.html_template: + html_templ = open(args.html_template, 'r').read() + html_temp = html_templ.replace('%type%', parts[0]) \ + .replace('%name%', var_name) \ + .replace('%default%', default_val) \ + .replace('%explanation%', htmlexpl) + htmlvar.append(html_temp) htmlexpl = "" - # Clean up variables - for var in [cppvar1, cppvar2, cppvar3, hvar1, hvar2, conf_example]: - var = var.rstrip() + # Convert lists to newline-separated strings + hvar1 = '\n'.join(hvar1) + hvar2 = '\n'.join(hvar2) + cppvar1 = '\n'.join(cppvar1) + cppvar2 = '\n'.join(cppvar2) + cppvar3 = '\n'.join(cppvar3) + conf_example = '\n\n'.join(conf_example) + htmlvar = ''.join(htmlvar) - # Read and write Configfile.cpp - with open('../src/Configfile.cpp.in', 'r') as f: - cpp_str = f.read() + # Write Configfile.cpp + if args.cpp_template and args.output_cpp: + try: + cpp_str = open(args.cpp_template, 'r').read() + cpp_str = cpp_str.replace('%templ_default_values%', cppvar1) \ + .replace('%templ_parsevars%', cppvar2) \ + .replace('%templ_getmethods%', cppvar3) - cpp_str = cpp_str.replace('%templ_default_values%', cppvar1) \ - .replace('%templ_parsevars%', cppvar2) \ - .replace('%templ_getmethods%', cppvar3) + os.makedirs(os.path.dirname(args.output_cpp), exist_ok=True) + with open(args.output_cpp, 'w') as f: + f.write(cpp_str) + except FileNotFoundError: + print(f"Error: C++ template file {args.cpp_template} not found.", file=sys.stderr) + sys.exit(1) - with open('Configfile.cpp', 'w') as f: - f.write(cpp_str) + # Write Configfile.h + if args.h_template and args.output_h: + try: + h_str = open(args.h_template, 'r').read() + h_str = h_str.replace('%templ_privateattribs%', hvar1) \ + .replace('%templ_publicmethods%', hvar2) - # Read and write Configfile.h - with open('../src/Configfile.h.in', 'r') as f: - h_str = f.read() - - h_str = h_str.replace('%templ_privateattribs%', hvar1) \ - .replace('%templ_publicmethods%', hvar2) - - with open('Configfile.h', 'w') as f: - f.write(h_str) + os.makedirs(os.path.dirname(args.output_h), exist_ok=True) + with open(args.output_h, 'w') as f: + f.write(h_str) + except FileNotFoundError: + print(f"Error: Header template file {args.h_template} not found.", file=sys.stderr) + sys.exit(1) # Write hermesrc.example - with open('../dists/hermesrc.example', 'w') as f: - f.write(conf_example) + if args.output_example: + os.makedirs(os.path.dirname(args.output_example), exist_ok=True) + with open(args.output_example, 'w') as f: + f.write(conf_example) # Write hermes-options.html - with open('../docs/hermes-options.html', 'w') as f: - f.write(htmlvar) + if args.output_html and htmlvar: + os.makedirs(os.path.dirname(args.output_html), exist_ok=True) + with open(args.output_html, 'w') as f: + f.write(htmlvar) if __name__ == "__main__": main() diff --git a/src/Configfile.tmpl b/src/Configfile.tmpl index 4cf022c..ef86599 100644 --- a/src/Configfile.tmpl +++ b/src/Configfile.tmpl @@ -9,7 +9,6 @@ * *clean* -#ifndef WIN32 * whether to fork to the background. initscripts require * this to be true most of the time. @@ -35,7 +34,6 @@ string,group,"nobody" * if you set background=true above, this will write the pid * of the forked hermes, not the original. string,pid_file,"/var/run/hermes.pid" -#endif //WIN32 * the port where hermes will listen for new connection. * if you are going to use a port lower than 1024 (almost always, @@ -58,11 +56,7 @@ int,server_port,2525 * database file to use. * if you are chrooting, the path is relative to the chroot: * real filepath = chroot + database_file -#ifdef WIN32 -string,database_file,"greylisting.db" -#else string,database_file,"/var/hermes/greylisting.db" -#endif //WIN32 * whether to use greylisting. * greylisting will slightly delay your emails (configurable, see below) @@ -120,13 +114,6 @@ bool,add_status_header,false * time to delay the initial SMTP banner int,banner_delay_time,5 -#ifdef REALLY_VERBOSE_DEBUG -* email to notify exceptions to. -* CAVEAT: the code that does this is VERY BUGGY and VERY VERBOSE, don't use unless you -* are a developer looking for a bug. -string,notify_to,"" -#endif //REALLY_VERBOSE_DEBUG - * greylisting options. * *clean* @@ -142,56 +129,12 @@ int,initial_blacklist,5 * 36 is a magic number, is the maximum days between a day and the same day next month int,whitelist_expiry,36 -* whether to submit stats. -bool,submit_stats,true - -* should stats be submited using SSL? -* recomended, but some people will compile without ssl. -#ifdef HAVE_SSL -bool,submit_stats_ssl,true -#else -bool,submit_stats_ssl,false -#endif //HAVE_SSL - -* username (used to submit stats). -* you can register on http://www.hermes-project.com -string,submit_stats_username,"anonymous" - -* password -string,submit_stats_password,"anonymous" - * log level: * 0: log only errors * 1: log errors and information (default) * 2: debug (passwords might be written in plaintext with this option, so use with care) int,log_level,1 -#if LOGGER_CLASS==FileLogger -* if you are using the filelogger, which file to log to. -string,file_logger_filename,"hermes.log" - -* whether to keep the logger file locked between writes -bool,keep_file_locked,true - -* frequency for log rotating in minutes -* default is 1440 (1 day) -* 0 means no rotation -int,log_rotation_frequency,1440 - -* format for the logfile rotation -* if you are using logfile rotation, file_logger represents the filename -* to which the logger will write, while this is the name files will get -* when rotated -* you can use the following variables: -* %%year%% - current year (4 digits) -* %%month%% - current month -* %%day%% - current day -* %%hour%% - current hour -* %%minute%% - current minute -* all of them are zero-padded -string,rotate_filename,"hermes-%%year%%-%%month%%-%%day%%-%%hour%%:%%minute%%.log" -#endif //LOGGER_CLASS==FileLogger - * whether to clean the database file and send stats. * if you have two instances of hermes running (for example one for smtp and other for smtps) * you want to configure all of them but one to use clean_db=false. @@ -201,9 +144,7 @@ string,rotate_filename,"hermes-%%year%%-%%month%%-%%day%%-%%hour%%:%%minute%%.lo * will be left hanging around without any use. bool,clean_db,true -#ifdef HAVE_SSL * ssl-related config options -* NOTE: this NEEDS the openssl library * *clean* @@ -235,7 +176,6 @@ string,certificate_file,"/etc/hermes/hermes.cert" * # openssl dhparam -out dhparam.pem * (replace with the number of bits suitable for you, e.G. 1024) string,dhparams_file,"" -#endif //HAVE_SSL * whether to add headers to the email sent or no. * to be rfc compatible this HAS to be true, but if you set to false, no one will know you are using hermes @@ -262,11 +202,7 @@ bool,check_helo_against_reverse,false * whether to query the spf record for the incoming domain. * should help, enable if you have libspf (if you don't, install it and recompile) -#ifdef HAVE_SPF bool,query_spf,true -#else -bool,query_spf,false -#endif //HAVE_SPF * return temporary error instead of permanent error. * Currently, this only applies to SPF and DNSBL rejected email From 3c8bd791e6fb8d03872aff82c5deec123b67976e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Guti=C3=A9rrez?= Date: Thu, 27 Mar 2025 08:29:26 +0100 Subject: [PATCH 06/12] More proxy stuff --- include/Proxy.h | 11 +- src/Proxy.cpp | 289 ++++++++++++++++++++++++++---------------------- 2 files changed, 163 insertions(+), 137 deletions(-) diff --git a/include/Proxy.h b/include/Proxy.h index 7e266c0..861f112 100644 --- a/include/Proxy.h +++ b/include/Proxy.h @@ -1,7 +1,8 @@ // Proxy.h #pragma once #include -#include "SocketInterface.h" +#include +#include #define SMTP_STATE_WAIT_FOR_HELO 0 #define SMTP_STATE_WAIT_FOR_MAILFROM 1 @@ -10,10 +11,10 @@ class Proxy { public: - Proxy(SocketInterface* outside_socket) : outside(outside_socket) {} - - void run(std::string& peer_address); + Proxy(); + void run(boost::asio::ssl::stream* outside); private: - SocketInterface* outside; + boost::asio::io_service& io_service_; + boost::asio::ssl::context ssl_context; }; diff --git a/src/Proxy.cpp b/src/Proxy.cpp index 1515869..9dac485 100644 --- a/src/Proxy.cpp +++ b/src/Proxy.cpp @@ -1,13 +1,17 @@ // Proxy.cpp #include "Proxy.h" +#include "HostnameResolver.h" #include #include -#include "Utils.h" // Dummy include; replace with actual Utils implementation -#include "Configfile.h" // Dummy include; replace with actual Configfile implementation +#include +#include +#include "Utils.h" +#include "Configfile.h" -extern Configfile cfg; // External configuration +extern Configfile cfg; -void Proxy::run(std::string& peer_address) { +void Proxy::run(boost::asio::ssl::stream* outside) { + boost::asio::ssl::stream* inside; // Original comments and variables retained std::string from = ""; std::string to = ""; @@ -16,7 +20,24 @@ void Proxy::run(std::string& peer_address) { unsigned char last_state = SMTP_STATE_WAIT_FOR_HELO; long unimplemented_requests = 0; + #ifdef HAVE_SPF + SpfChecker spf_checker; + #endif + try { + // Resolve hostname using the Boost resolver + try { + resolvedname = HostnameResolver::resolveHostname(peer_address); + } + catch (const std::exception& e) { + std::cerr << std::format("Hostname resolution error: {}", e.what()) << std::endl; + resolvedname = ""; + } + + // Configure SSL contexts + boost::asio::ssl::context ssl_context(boost::asio::ssl::context::tlsv12); + ssl_context.set_verify_mode(boost::asio::ssl::verify_none); + bool throttled = cfg.getThrottle(); // Start with a throttled connection bool authenticated = false; // Start with a non-authenticated connection bool esmtp = false; @@ -24,7 +45,7 @@ void Proxy::run(std::string& peer_address) { std::string hermes_status = "unknown"; // Check whitelist - if (!cfg.getDnsWhitelistDomains().empty() && + if (!cfg.getDnsWhitelistDomains().empty() && Utils::listed_on_dns_lists(cfg.getDnsWhitelistDomains(), cfg.getDnsWhitelistPercentage(), peer_address)) { authenticated = true; hermes_status = "whitelisted"; @@ -39,195 +60,199 @@ void Proxy::run(std::string& peer_address) { } 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"); + + // Check if data is waiting before server banner + boost::system::error_code ec; + size_t available = outside->lowest_layer().available(ec); + if (ec || available > 0) { + std::cout << std::format("421 (data_before_banner) (ip:{})\n", peer_address); + std::this_thread::sleep_for(std::chrono::seconds(20)); + + // Write rejection message + std::string rejection_msg = "421 Stop sending data before we show you the banner\r\n"; + boost::asio::write(outside->lowest_layer(), boost::asio::buffer(rejection_msg), ec); return; } } } // Connect to the inside server - inside.connect(cfg.getServerHost(), cfg.getServerPort()); + boost::asio::ip::tcp::resolver resolver(io_service_); + boost::asio::ip::tcp::resolver::results_type endpoints = + resolver.resolve(cfg.getServerHost(), std::to_string(cfg.getServerPort())); - #ifdef HAVE_SSL + boost::asio::connect(inside.lowest_layer(), endpoints); + + // SSL setup if (cfg.getOutgoingSsl()) { - inside.prepareSSL(false); - inside.startSSL(false); + inside.set_verify_mode(boost::asio::ssl::verify_none); + inside.handshake(boost::asio::ssl::stream_base::client); } + if (cfg.getIncomingSsl()) { - outside->prepareSSL(true); - outside->startSSL(true); + outside->set_verify_mode(boost::asio::ssl::verify_none); + outside->handshake(boost::asio::ssl::stream_base::server); } - #endif // HAVE_SSL + + // Communication buffers + std::vector read_buffer(4096); + boost::system::error_code ec; // Main loop for communication - while (!outside->isClosed() && !inside.isClosed()) { + while (!outside->lowest_layer().is_open() || !inside.lowest_layer().is_open()) { // Check if the client wants to send something to the server - if (outside->canRead(0.2)) { - strtemp = outside->readLine(); - if (outside->isClosed()) return; + size_t client_available = outside->lowest_layer().available(ec); + if (client_available > 0) { + size_t bytes_read = outside->read_some(boost::asio::buffer(read_buffer), ec); + strtemp = std::string(read_buffer.begin(), read_buffer.begin() + bytes_read); 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)) || + + 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; } + + // RCPT TO handling with comprehensive checks 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 = ""; - } + // Construct log string using std::format + std::string strlog = std::format( + "from {} (ip:{}, hostname:{}, {} {}:{}) -> to {}", + from, + peer_address, + resolvedname, + (esmtp ? "ehlo" : "helo"), + ehlostr, + to + ); - 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)) { + // Greylisting check + std::string code = "250"; + 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"; + message = std::format("{} Greylisted!! Please try again in a few minutes.", code); + std::cout << std::format("checking {}\n", mechanism); } + // SPF Check #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"; + else if (cfg.getQuerySpf() && !authenticated && + !spf_checker.query(peer_address, ehlostr, from)) { + code = cfg.getAddStatusHeader() ? "250" : + (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"; + message = std::format( + "{} You do not seem to be allowed to send email for that particular domain.", + code + ); + std::cout << std::format("checking {}\n", mechanism); } - #endif // HAVE_SPF - // Check blacklist - else if (!authenticated && Utils::blacklisted(cfg.getDatabaseFile(), peer_address, to)) { + #endif + // Blacklist check + 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"; + message = std::format( + "{} You do not seem to be allowed to send email to that particular domain from that address.", + code + ); + std::cout << std::format("checking {}\n", 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"; + // DNS Blacklist check + else if (!cfg.getDnsBlacklistDomains().empty() && !authenticated && + Utils::listed_on_dns_lists(cfg.getDnsBlacklistDomains(), + cfg.getDnsBlacklistPercentage(), + peer_address)) { + code = cfg.getAddStatusHeader() ? "250" : + (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"; + message = std::format( + "{} You are listed on some DNS blacklists. Get delisted before trying to send us email.", + code + ); + std::cout << std::format("checking {}\n", mechanism); } - else if (cfg.getRejectNoReverseResolution() && !authenticated && "" == resolvedname) { + // Reverse DNS check + else if (cfg.getRejectNoReverseResolution() && !authenticated && resolvedname.empty()) { 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"; + message = std::format( + "{} Your IP address does not resolve to a hostname.", + code + ); + std::cout << std::format("checking {}\n", mechanism); } + // HELO/Reverse name check 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"; + message = std::format( + "{} Your IP hostname doesn't match your envelope hostname.", + code + ); + std::cout << std::format("checking {}\n", mechanism); } - if (!mechanism.empty()) strlog.insert(0, "(" + mechanism + ") "); - strlog.insert(0, code + " "); - std::cout << strlog << "\n"; // Log the connection + // Prepare log message + if (!mechanism.empty()) { + strlog = std::format("({}) {}", mechanism, strlog); + } + strlog = std::format("{} {}", code, strlog); + std::cout << strlog << "\n"; - // 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 + // Handle rejection + if (code != "250") { + // Close inside connection + inside.lowest_layer().close(); + + // Delay to annoy spammers std::this_thread::sleep_for(std::chrono::seconds(20)); - outside->writeLine(message); + + // Send rejection message + std::string rejection_msg = message + "\r\n"; + boost::asio::write(outside->lowest_layer(), boost::asio::buffer(rejection_msg), ec); 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); + // Send to inside server + boost::asio::write(inside.lowest_layer(), boost::asio::buffer(strtemp), ec); } // Check if the server wants to send something to the client - if (inside.canRead(0.2)) { - strtemp = inside.readLine(); - if (inside.isClosed()) return; + size_t server_available = inside.lowest_layer().available(ec); + if (server_available > 0) { + size_t bytes_read = inside.read_some(boost::asio::buffer(read_buffer), ec); + strtemp = std::string(read_buffer.begin(), read_buffer.begin() + bytes_read); - 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); + // Send to outside socket + boost::asio::write(outside->lowest_layer(), boost::asio::buffer(strtemp), ec); } - if (throttled) std::this_thread::sleep_for(std::chrono::seconds(cfg.getThrottlingTime())); // Take 1 second between each command + // Throttling + if (throttled) { + std::this_thread::sleep_for(std::chrono::seconds(cfg.getThrottlingTime())); + } } - } catch (Exception& e) { // Any exception will close both connections - std::cerr << "Exception occurred: " << e.what() << std::endl; - return; + } + catch (const boost::system::system_error& e) { + std::cerr << std::format("Boost.Asio error: {}", e.what()) << std::endl; + } + catch (const std::exception& e) { + std::cerr << std::format("Standard exception: {}", e.what()) << std::endl; } } From b4b995dd19d20e18322a5fd82071b136e3f9aadc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Guti=C3=A9rrez=20de=20Quevedo=20P=C3=A9?= =?UTF-8?q?rez?= Date: Fri, 28 Mar 2025 10:34:52 +0100 Subject: [PATCH 07/12] more stuff --- include/Proxy.h | 5 +++-- src/Proxy.cpp | 3 +-- src/Utils.cpp | 9 ++++----- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/include/Proxy.h b/include/Proxy.h index 861f112..72c0033 100644 --- a/include/Proxy.h +++ b/include/Proxy.h @@ -9,10 +9,11 @@ #define SMTP_STATE_WAIT_FOR_RCPTTO 2 #define SMTP_STATE_WAIT_FOR_DATA 3 -class Proxy { +class Proxy +{ public: Proxy(); - void run(boost::asio::ssl::stream* outside); + void run(boost::asio::ssl::stream* outside, const std::string& peer_address); private: boost::asio::io_service& io_service_; diff --git a/src/Proxy.cpp b/src/Proxy.cpp index 9dac485..174b4a4 100644 --- a/src/Proxy.cpp +++ b/src/Proxy.cpp @@ -1,6 +1,5 @@ // Proxy.cpp #include "Proxy.h" -#include "HostnameResolver.h" #include #include #include @@ -10,7 +9,7 @@ extern Configfile cfg; -void Proxy::run(boost::asio::ssl::stream* outside) { +void Proxy::run(boost::asio::ssl::stream* outside, const std::string& peer_address) { boost::asio::ssl::stream* inside; // Original comments and variables retained std::string from = ""; diff --git a/src/Utils.cpp b/src/Utils.cpp index 20c3631..b20d5bb 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -67,12 +67,11 @@ string Utils::ulongtostr(unsigned long number) * @return string lowercase version of s * */ -string Utils::strtolower(string s) +string Utils::strtolower(const std::string_view s) { - for(unsigned int i=0;i Date: Fri, 28 Mar 2025 17:12:20 +0100 Subject: [PATCH 08/12] refactor: modernize networking code with Boost.Asio - Replace raw socket implementation with Boost.Asio in Proxy class - Add proper SSL/TLS support using Boost.Asio SSL - Improve error handling with more specific exceptions - Modernize Utils class with C++17 features like string_view - Refactor Windows service implementation with smart pointers and exception handling - Enhance hostname resolution with Boost.Asio resolver --- include/Proxy.h | 10 +- include/Utils.h | 37 ++-- src/Proxy.cpp | 344 +++++++++++++++++++------------------ src/Utils.cpp | 75 ++++---- src/win32-service.cpp | 388 ++++++++++++++++++++---------------------- 5 files changed, 432 insertions(+), 422 deletions(-) diff --git a/include/Proxy.h b/include/Proxy.h index 72c0033..ee5d76a 100644 --- a/include/Proxy.h +++ b/include/Proxy.h @@ -3,19 +3,21 @@ #include #include #include +#include "Socket.h" #define SMTP_STATE_WAIT_FOR_HELO 0 #define SMTP_STATE_WAIT_FOR_MAILFROM 1 #define SMTP_STATE_WAIT_FOR_RCPTTO 2 #define SMTP_STATE_WAIT_FOR_DATA 3 -class Proxy -{ +class Proxy { public: Proxy(); - void run(boost::asio::ssl::stream* outside, const std::string& peer_address); + void setOutside(Socket& socket); + void run(const std::string& peer_address); private: - boost::asio::io_service& io_service_; + boost::asio::io_context io_context_; boost::asio::ssl::context ssl_context; + Socket* outside_socket_; }; diff --git a/include/Utils.h b/include/Utils.h index ad05f1f..9a3fa9f 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -22,6 +22,7 @@ #include "hermes.h" #include +#include #include #include #include @@ -35,7 +36,9 @@ #include "Database.h" #include "Socket.h" -using namespace std; +using std::string; +using std::stringstream; +using std::list; #ifdef WIN32 #define sleep(x) Sleep(1000*(x)) @@ -52,37 +55,37 @@ class Utils { public: //string utilities - static string strtolower(string); - static string trim(string); + static string strtolower(std::string_view); + static string trim(std::string_view); static string inttostr(int); static string ulongtostr(unsigned long); //email-related utilities - static string getmail(string&); - static string getdomain(string&); - static string reverseip(string&); + static string getmail(const string&); + static string getdomain(const string&); + static string reverseip(const string&); //spam-related utilities (TODO: move to a different class) - static bool greylist(string,string&,string&,string&); - static bool listed_on_dns_lists(list&,unsigned char,string&); - static bool whitelisted(string,string&); - static bool blacklisted(string,string&,string&); + static bool greylist(const string& dbfile, string& ip, string& p_from, string& p_to); + static bool listed_on_dns_lists(const list& dns_domains, unsigned char percentage, const string& ip); + static bool whitelisted(const string& dbfile, string& ip); + static bool blacklisted(const string& dbfile, string& ip, string& to); #ifndef WIN32 //posix-utils - static int usertouid(string); - static int grouptogid(string); + static int usertouid(const string& user); + static int grouptogid(const string& groupname); #endif //WIN32 //misc - static string get_canonical_filename(string); - static bool file_exists(string); - static bool dir_exists(string); + static string get_canonical_filename(const string& file); + static bool file_exists(const string& file); + static bool dir_exists(const string& dir); static string errnotostrerror(int); static string rfc2821_date(time_t *timestamp=NULL); static string gethostname(); - static void write_pid(string,pid_t); - static string gethostname(int s); + static void write_pid(const string& file, pid_t pid); + static string gethostname(int socket); }; #endif //UTILS_H diff --git a/src/Proxy.cpp b/src/Proxy.cpp index 174b4a4..415a7a1 100644 --- a/src/Proxy.cpp +++ b/src/Proxy.cpp @@ -2,20 +2,53 @@ #include "Proxy.h" #include #include -#include +#include +#include #include +#include #include "Utils.h" #include "Configfile.h" extern Configfile cfg; -void Proxy::run(boost::asio::ssl::stream* outside, const std::string& peer_address) { - boost::asio::ssl::stream* inside; +namespace { +std::string resolveHostname(const std::string& ip_address) { + try { + boost::asio::io_context io_context; + boost::asio::ip::tcp::resolver resolver(io_context); + boost::asio::ip::address addr = boost::asio::ip::make_address(ip_address); + boost::asio::ip::tcp::endpoint ep(addr, 0); + auto results = resolver.resolve(ep.address().to_string(), ""); + if (results.begin() != results.end()) { + return results.begin()->host_name(); + } + } catch (const std::exception&) {} + return ""; +} +} + +Proxy::Proxy() + : io_context_(), + ssl_context(boost::asio::ssl::context::tlsv12), + outside_socket_(nullptr) { +} + +void Proxy::setOutside(Socket& socket) { + outside_socket_ = &socket; +} + +void Proxy::run(const std::string& peer_address) { + if (!outside_socket_) { + throw std::runtime_error("Outside socket not set"); + } + + boost::asio::ssl::stream inside(io_context_, ssl_context); // Original comments and variables retained - std::string from = ""; - std::string to = ""; - std::string ehlostr = ""; - std::string resolvedname = ""; + const std::string empty_str; + std::string from = empty_str; + std::string to = empty_str; + std::string ehlostr = empty_str; + std::string resolvedname = empty_str; unsigned char last_state = SMTP_STATE_WAIT_FOR_HELO; long unimplemented_requests = 0; @@ -25,13 +58,7 @@ void Proxy::run(boost::asio::ssl::stream* outside, try { // Resolve hostname using the Boost resolver - try { - resolvedname = HostnameResolver::resolveHostname(peer_address); - } - catch (const std::exception& e) { - std::cerr << std::format("Hostname resolution error: {}", e.what()) << std::endl; - resolvedname = ""; - } + resolvedname = resolveHostname(peer_address); // Configure SSL contexts boost::asio::ssl::context ssl_context(boost::asio::ssl::context::tlsv12); @@ -53,7 +80,8 @@ void Proxy::run(boost::asio::ssl::stream* outside, } } - if (cfg.getWhitelistedDisablesEverything() && Utils::whitelisted(cfg.getDatabaseFile(), peer_address)) { + std::string peer_addr_copy = peer_address; + if (cfg.getWhitelistedDisablesEverything() && Utils::whitelisted(cfg.getDatabaseFile(), peer_addr_copy)) { throttled = false; authenticated = true; } else { @@ -61,22 +89,20 @@ void Proxy::run(boost::asio::ssl::stream* outside, std::this_thread::sleep_for(std::chrono::seconds(cfg.getBannerDelayTime())); // Check if data is waiting before server banner - boost::system::error_code ec; - size_t available = outside->lowest_layer().available(ec); - if (ec || available > 0) { - std::cout << std::format("421 (data_before_banner) (ip:{})\n", peer_address); + if (outside_socket_->canRead(0.0)) { + std::cout << fmt::format("421 (data_before_banner) (ip:{})\n", peer_address); std::this_thread::sleep_for(std::chrono::seconds(20)); // Write rejection message - std::string rejection_msg = "421 Stop sending data before we show you the banner\r\n"; - boost::asio::write(outside->lowest_layer(), boost::asio::buffer(rejection_msg), ec); + std::string rejection_msg = fmt::format("421 Stop sending data before we show you the banner\r\n"); + outside_socket_->writeBytes(const_cast(rejection_msg.c_str()), rejection_msg.length()); return; } } } // Connect to the inside server - boost::asio::ip::tcp::resolver resolver(io_service_); + boost::asio::ip::tcp::resolver resolver(io_context_); boost::asio::ip::tcp::resolver::results_type endpoints = resolver.resolve(cfg.getServerHost(), std::to_string(cfg.getServerPort())); @@ -89,159 +115,151 @@ void Proxy::run(boost::asio::ssl::stream* outside, } if (cfg.getIncomingSsl()) { - outside->set_verify_mode(boost::asio::ssl::verify_none); - outside->handshake(boost::asio::ssl::stream_base::server); + #ifdef HAVE_SSL + outside_socket_->prepareSSL(true); + outside_socket_->startSSL(true); + #endif } // Communication buffers - std::vector read_buffer(4096); - boost::system::error_code ec; + char read_buffer[4096]; + ssize_t bytes_read; // Main loop for communication - while (!outside->lowest_layer().is_open() || !inside.lowest_layer().is_open()) { + while (!outside_socket_->isClosed() && !inside.lowest_layer().is_open()) { // Check if the client wants to send something to the server - size_t client_available = outside->lowest_layer().available(ec); - if (client_available > 0) { - size_t bytes_read = outside->read_some(boost::asio::buffer(read_buffer), ec); - strtemp = std::string(read_buffer.begin(), read_buffer.begin() + bytes_read); + if (outside_socket_->canRead(1.0)) { + bytes_read = outside_socket_->readBytes(read_buffer, sizeof(read_buffer)); + if (bytes_read > 0) { + strtemp = std::string(read_buffer, bytes_read); - if (strtemp.length() > 10 && "mail from:" == Utils::strtolower(strtemp.substr(0, 10))) { - from = Utils::getmail(strtemp); - last_state = SMTP_STATE_WAIT_FOR_RCPTTO; + std::string cmd_lower = Utils::strtolower(strtemp.substr(0, std::min(10, strtemp.length()))); + if (strtemp.length() > 10 && "mail from:" == cmd_lower) { + from = std::string(Utils::getmail(strtemp)); + last_state = SMTP_STATE_WAIT_FOR_RCPTTO; + } + + cmd_lower = Utils::strtolower(strtemp.substr(0, std::min(4, strtemp.length()))); + if ("ehlo" == cmd_lower) esmtp = true; + + if (strtemp.length() > 4 && ("ehlo" == cmd_lower || + "helo" == cmd_lower)) { + ehlostr = std::string(Utils::trim(strtemp.substr(5))); + last_state = SMTP_STATE_WAIT_FOR_MAILFROM; + } + + // RCPT TO handling with comprehensive checks + cmd_lower = Utils::strtolower(strtemp.substr(0, std::min(8, strtemp.length()))); + if (strtemp.length() > 8 && "rcpt to:" == cmd_lower) { + std::string mechanism; + std::string message; + to = std::string(Utils::getmail(strtemp)); + + // Construct log string + std::string strlog = fmt::format("from {} (ip:{}, hostname:{}, {} {}) -> to {}", + from, peer_address, resolvedname, (esmtp ? "ehlo" : "helo"), ehlostr, to); + + // Greylisting check + std::string code = "250"; + std::string peer_addr_copy = peer_address; + std::string from_copy = from; + std::string to_copy = to; + if (cfg.getGreylist() && !authenticated && + Utils::greylist(cfg.getDatabaseFile(), peer_addr_copy, from_copy, to_copy)) { + code = "421"; + mechanism = "greylist"; + message = fmt::format("{} Greylisted!! Please try again in a few minutes.", code); + std::cout << fmt::format("checking {}\n", mechanism); + } + // SPF Check + #ifdef HAVE_SPF + else if (cfg.getQuerySpf() && !authenticated && + !spf_checker.query(peer_address, ehlostr, from)) { + code = cfg.getAddStatusHeader() ? "250" : + (cfg.getReturnTempErrorOnReject() ? "421" : "550"); + mechanism = "spf"; + message = fmt::format("{} You do not seem to be allowed to send email for that particular domain.", code); + std::cout << fmt::format("checking {}\n", mechanism); + } + #endif + // Blacklist check + else if (!authenticated && + Utils::blacklisted(cfg.getDatabaseFile(), peer_addr_copy, to_copy)) { + code = cfg.getReturnTempErrorOnReject() ? "421" : "550"; + mechanism = "allowed-domain-per-ip"; + message = fmt::format("{} You do not seem to be allowed to send email to that particular domain from that address.", code); + std::cout << fmt::format("checking {}\n", mechanism); + } + // DNS Blacklist check + else if (!cfg.getDnsBlacklistDomains().empty() && !authenticated && + Utils::listed_on_dns_lists(cfg.getDnsBlacklistDomains(), + cfg.getDnsBlacklistPercentage(), + peer_address)) { + code = cfg.getAddStatusHeader() ? "250" : + (cfg.getReturnTempErrorOnReject() ? "421" : "550"); + mechanism = "dnsbl"; + message = fmt::format("{} You are listed on some DNS blacklists. Get delisted before trying to send us email.", code); + std::cout << fmt::format("checking {}\n", mechanism); + } + // Reverse DNS check + else if (cfg.getRejectNoReverseResolution() && !authenticated && resolvedname.empty()) { + code = cfg.getReturnTempErrorOnReject() ? "421" : "550"; + mechanism = "no reverse resolution"; + message = fmt::format("{} Your IP address does not resolve to a hostname.", code); + std::cout << fmt::format("checking {}\n", mechanism); + } + // HELO/Reverse name check + else if (cfg.getCheckHeloAgainstReverse() && !authenticated && ehlostr != resolvedname) { + code = cfg.getReturnTempErrorOnReject() ? "421" : "550"; + mechanism = "helo differs from resolved name"; + message = fmt::format("{} Your IP hostname doesn't match your envelope hostname.", code); + std::cout << fmt::format("checking {}\n", mechanism); + } + + // Prepare log message + std::string log_str = strlog; + if (!mechanism.empty()) { + log_str = fmt::format("({}) {}", mechanism, log_str); + } + std::cout << fmt::format("{} {}\n", code, log_str); + + // Handle rejection + if (code != "250") { + // Close inside connection + inside.lowest_layer().close(); + + // Delay to annoy spammers + std::this_thread::sleep_for(std::chrono::seconds(20)); + + // Send rejection message + std::string rejection_msg = fmt::format("{}\r\n", message); + outside_socket_->writeBytes(const_cast(rejection_msg.c_str()), rejection_msg.length()); + return; + } + + last_state = SMTP_STATE_WAIT_FOR_DATA; + } + + // Send to inside server + boost::asio::async_write(inside, boost::asio::buffer(strtemp), + [](const boost::system::error_code& /*error*/, std::size_t /*bytes_transferred*/) {}); } - - 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; - } - - // RCPT TO handling with comprehensive checks - if (strtemp.length() > 8 && "rcpt to:" == Utils::strtolower(strtemp.substr(0, 8))) { - std::string mechanism = ""; - std::string message = ""; - to = Utils::getmail(strtemp); - - // Construct log string using std::format - std::string strlog = std::format( - "from {} (ip:{}, hostname:{}, {} {}:{}) -> to {}", - from, - peer_address, - resolvedname, - (esmtp ? "ehlo" : "helo"), - ehlostr, - to - ); - - // Greylisting check - std::string code = "250"; - if (cfg.getGreylist() && !authenticated && - Utils::greylist(cfg.getDatabaseFile(), peer_address, from, to)) { - code = "421"; - mechanism = "greylist"; - message = std::format("{} Greylisted!! Please try again in a few minutes.", code); - std::cout << std::format("checking {}\n", mechanism); - } - // SPF Check - #ifdef HAVE_SPF - else if (cfg.getQuerySpf() && !authenticated && - !spf_checker.query(peer_address, ehlostr, from)) { - code = cfg.getAddStatusHeader() ? "250" : - (cfg.getReturnTempErrorOnReject() ? "421" : "550"); - mechanism = "spf"; - message = std::format( - "{} You do not seem to be allowed to send email for that particular domain.", - code - ); - std::cout << std::format("checking {}\n", mechanism); - } - #endif - // Blacklist check - else if (!authenticated && - Utils::blacklisted(cfg.getDatabaseFile(), peer_address, to)) { - code = cfg.getReturnTempErrorOnReject() ? "421" : "550"; - mechanism = "allowed-domain-per-ip"; - message = std::format( - "{} You do not seem to be allowed to send email to that particular domain from that address.", - code - ); - std::cout << std::format("checking {}\n", mechanism); - } - // DNS Blacklist check - else if (!cfg.getDnsBlacklistDomains().empty() && !authenticated && - Utils::listed_on_dns_lists(cfg.getDnsBlacklistDomains(), - cfg.getDnsBlacklistPercentage(), - peer_address)) { - code = cfg.getAddStatusHeader() ? "250" : - (cfg.getReturnTempErrorOnReject() ? "421" : "550"); - mechanism = "dnsbl"; - message = std::format( - "{} You are listed on some DNS blacklists. Get delisted before trying to send us email.", - code - ); - std::cout << std::format("checking {}\n", mechanism); - } - // Reverse DNS check - else if (cfg.getRejectNoReverseResolution() && !authenticated && resolvedname.empty()) { - code = cfg.getReturnTempErrorOnReject() ? "421" : "550"; - mechanism = "no reverse resolution"; - message = std::format( - "{} Your IP address does not resolve to a hostname.", - code - ); - std::cout << std::format("checking {}\n", mechanism); - } - // HELO/Reverse name check - else if (cfg.getCheckHeloAgainstReverse() && !authenticated && ehlostr != resolvedname) { - code = cfg.getReturnTempErrorOnReject() ? "421" : "550"; - mechanism = "helo differs from resolved name"; - message = std::format( - "{} Your IP hostname doesn't match your envelope hostname.", - code - ); - std::cout << std::format("checking {}\n", mechanism); - } - - // Prepare log message - if (!mechanism.empty()) { - strlog = std::format("({}) {}", mechanism, strlog); - } - strlog = std::format("{} {}", code, strlog); - std::cout << strlog << "\n"; - - // Handle rejection - if (code != "250") { - // Close inside connection - inside.lowest_layer().close(); - - // Delay to annoy spammers - std::this_thread::sleep_for(std::chrono::seconds(20)); - - // Send rejection message - std::string rejection_msg = message + "\r\n"; - boost::asio::write(outside->lowest_layer(), boost::asio::buffer(rejection_msg), ec); - return; - } - - last_state = SMTP_STATE_WAIT_FOR_DATA; - } - - // Send to inside server - boost::asio::write(inside.lowest_layer(), boost::asio::buffer(strtemp), ec); } // Check if the server wants to send something to the client + boost::system::error_code ec; size_t server_available = inside.lowest_layer().available(ec); - if (server_available > 0) { - size_t bytes_read = inside.read_some(boost::asio::buffer(read_buffer), ec); - strtemp = std::string(read_buffer.begin(), read_buffer.begin() + bytes_read); - - // Send to outside socket - boost::asio::write(outside->lowest_layer(), boost::asio::buffer(strtemp), ec); + if (!ec && server_available > 0) { + std::vector server_buffer(server_available); + size_t bytes_read = boost::asio::read(inside, boost::asio::buffer(server_buffer), ec); + if (!ec && bytes_read > 0) { + outside_socket_->writeBytes(server_buffer.data(), bytes_read); + } } + // Run io_context to process async operations + io_context_.poll(); + // Throttling if (throttled) { std::this_thread::sleep_for(std::chrono::seconds(cfg.getThrottlingTime())); @@ -249,9 +267,9 @@ void Proxy::run(boost::asio::ssl::stream* outside, } } catch (const boost::system::system_error& e) { - std::cerr << std::format("Boost.Asio error: {}", e.what()) << std::endl; + std::cerr << fmt::format("Boost.Asio error: {}\n", e.what()); } catch (const std::exception& e) { - std::cerr << std::format("Standard exception: {}", e.what()) << std::endl; + std::cerr << fmt::format("Standard exception: {}\n", e.what()); } } diff --git a/src/Utils.cpp b/src/Utils.cpp index b20d5bb..c553416 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -21,6 +21,11 @@ #include #include +#include + +using std::string; +using std::stringstream; +using std::list; extern Configfile cfg; extern LOGGER_CLASS hermes_log; @@ -69,9 +74,8 @@ string Utils::ulongtostr(unsigned long number) */ string Utils::strtolower(const std::string_view s) { - const std::string lower_str = boost::algorithm::to_lower_copy(s); - - return lower_str; + const std::string str(s); + return boost::algorithm::to_lower_copy(str); } /** @@ -84,13 +88,13 @@ string Utils::strtolower(const std::string_view s) */ string Utils::trim(const std::string_view s) { - while(isspace(s[0])) - s.erase(0,1); - - while(isspace(s[s.length()-1])) - s.erase(s.length()-1,1); - - return s; + auto start = s.find_first_not_of(" \t\n\r\f\v"); + if (start == std::string_view::npos) { + return string(); + } + + auto end = s.find_last_not_of(" \t\n\r\f\v"); + return string(s.substr(start, end - start + 1)); } //------------------------ @@ -141,7 +145,7 @@ string Utils::trim(const std::string_view s) * @return whether triplet should get greylisted or not * @todo unify {white,black,grey}list in one function that returns a different constant in each case */ -bool Utils::greylist(string dbfile,string& ip,string& p_from,string& p_to) +bool Utils::greylist(const string& dbfile, string& ip, string& p_from, string& p_to) { string from=Database::cleanString(p_from); string to=Database::cleanString(p_to); @@ -180,7 +184,7 @@ bool Utils::greylist(string dbfile,string& ip,string& p_from,string& p_to) * @return whether ip is whitelisted or not * @todo unify {white,black,grey}list in one function that returns a different constant in each case */ -bool Utils::whitelisted(string dbfile,string& ip) +bool Utils::whitelisted(const string& dbfile, string& ip) { Database db; string hostname; @@ -212,7 +216,7 @@ bool Utils::whitelisted(string dbfile,string& ip) * @return whether ip is whitelisted or not * @todo this should contain all cases when we should reject a connection */ -bool Utils::blacklisted(string dbfile,string& ip,string& to) +bool Utils::blacklisted(const string& dbfile, string& ip, string& to) { Database db; string hostname; @@ -233,7 +237,7 @@ bool Utils::blacklisted(string dbfile,string& ip,string& to) * @return email extracted from rawline * */ -string Utils::getmail(string& rawline) +string Utils::getmail(const string& rawline) { string email; string::size_type start=0,end=0; @@ -279,7 +283,7 @@ string Utils::getmail(string& rawline) * @return domain of email * */ -string Utils::getdomain(string& email) +string Utils::getdomain(const string& email) { if(email.rfind('@')) return trim(email.substr(email.rfind('@')+1)); @@ -306,7 +310,7 @@ string Utils::getdomain(string& email) * @return uid for user * */ -int Utils::usertouid(string user) +int Utils::usertouid(const string& user) { struct passwd *pwd; pwd=getpwnam(user.c_str()); @@ -329,7 +333,7 @@ int Utils::usertouid(string user) * @return gid for groupname * */ -int Utils::grouptogid(string groupname) +int Utils::grouptogid(const string& groupname) { struct group *grp; grp=getgrnam(groupname.c_str()); @@ -352,7 +356,7 @@ int Utils::grouptogid(string groupname) * @return is file readable? * */ -bool Utils::file_exists(string file) +bool Utils::file_exists(const string& file) { FILE *f=fopen(file.c_str(),"r"); if(NULL==f) @@ -365,7 +369,7 @@ bool Utils::file_exists(string file) } #ifdef WIN32 -string Utils::get_canonical_filename(string file) +string Utils::get_canonical_filename(const string& file) { char buffer[MAX_PATH]; @@ -374,7 +378,7 @@ string Utils::get_canonical_filename(string file) return string(buffer); } #else -string Utils::get_canonical_filename(string file) +string Utils::get_canonical_filename(const string& file) { char *buffer=NULL; string result; @@ -386,6 +390,7 @@ string Utils::get_canonical_filename(string file) return result; } #endif //WIN32 + /** * whether a directory is accesible by current process/user * @@ -394,7 +399,7 @@ string Utils::get_canonical_filename(string file) * @return isdir readable? * */ -bool Utils::dir_exists(string dir) +bool Utils::dir_exists(const string& dir) { DIR *d=opendir(dir.c_str()); if(NULL==d) @@ -431,8 +436,6 @@ string Utils::errnotostrerror(int errnum) strerr="Error "; #endif //WIN32 return string(strerr)+" ("+inttostr(errnum)+")"; -// else -// return string("Error "+inttostr(errno)+" retrieving error code for error number ")+inttostr(errnum); } /** @@ -458,7 +461,7 @@ string Utils::errnotostrerror(int errnum) * * @return whether ip is blacklisted or not */ -bool Utils::listed_on_dns_lists(list& dns_domains,unsigned char percentage,string& ip) +bool Utils::listed_on_dns_lists(const list& dns_domains, unsigned char percentage, const string& ip) { string reversedip; unsigned char number_of_lists=dns_domains.size(); @@ -467,7 +470,7 @@ bool Utils::listed_on_dns_lists(list& dns_domains,unsigned char percenta reversedip=reverseip(ip); - for(list::iterator i=dns_domains.begin();i!=dns_domains.end();i++) + for(list::const_iterator i=dns_domains.begin();i!=dns_domains.end();i++) { string dns_domain; @@ -514,7 +517,7 @@ bool Utils::listed_on_dns_lists(list& dns_domains,unsigned char percenta * * @return the reversed ip */ -string Utils::reverseip(string& ip) +string Utils::reverseip(const string& ip) { string inverseip=""; string::size_type pos=0,ppos=0; @@ -628,13 +631,19 @@ string Utils::gethostname() return string(buf); } -string Utils::gethostname(int s) +/** + * Get the hostname for a given socket + * + * @param socket Socket to get hostname for + * @return The hostname for the socket + */ +string Utils::gethostname(int socket) { struct sockaddr_in sa; unsigned int dummy = sizeof sa; struct hostent *hp; - if (getsockname(s,(struct sockaddr *) &sa,&dummy) == -1) + if (getsockname(socket,(struct sockaddr *) &sa,&dummy) == -1) throw Exception("Error getting ip from socket"+Utils::errnotostrerror(errno),__FILE__,__LINE__); hp=gethostbyaddr((const void *)&sa.sin_addr,sizeof sa.sin_addr,AF_INET); @@ -645,8 +654,13 @@ string Utils::gethostname(int s) return string(hp->h_name); } - -void Utils::write_pid(string file,pid_t pid) +/** + * Write process ID to a file + * + * @param file File to write PID to + * @param pid Process ID to write + */ +void Utils::write_pid(const string& file, pid_t pid) { FILE *f; @@ -657,4 +671,3 @@ void Utils::write_pid(string file,pid_t pid) fprintf(f,"%d\n",pid); fclose(f); } - diff --git a/src/win32-service.cpp b/src/win32-service.cpp index 43d71eb..358bae4 100644 --- a/src/win32-service.cpp +++ b/src/win32-service.cpp @@ -18,247 +18,221 @@ * @author Juan José Gutiérrez de Quevedo */ #include +#include +#include #include -using namespace std; +namespace { + // Service configuration constants + constexpr const char* SERVICE_NAME = "hermes anti-spam proxy"; + constexpr const char* SERVICE_SHORT_NAME = "hermes"; + constexpr const char* SERVICE_DESCRIPTION_TEXT = + "An anti-spam proxy using a combination of techniques like greylisting, dnsbl/dnswl, SPF, etc."; -#define SERVICE_NAME "hermes anti-spam proxy" -#define SERVICE_SHORT_NAME "hermes" -#define SERVICE_DESCRIPTION_TEXT "An anti-spam proxy using a combination of techniques like greylisting, dnsbl/dnswl, SPF, etc." + // Global state + SERVICE_STATUS service_status; + SERVICE_STATUS_HANDLE service_status_handle; + extern bool quit; -//macros -#define ChangeServiceStatus(x,y,z) y.dwCurrentState=z; SetServiceStatus(x,&y); -#define MIN(x,y) (((x)<(y))?(x):(y)) -#define cmp(x,y) strncmp(x,y,strlen(y)) -#define msgbox(x,y,z) MessageBox(NULL,x,z SERVICE_NAME,MB_OK|y) -#define winerror(x) msgbox(x,MB_ICONERROR,"Error from ") -#define winmessage(x) msgbox(x,MB_ICONINFORMATION,"Message from ") -#define condfree(x) if(NULL!=x) free(x); -#define safemalloc(x,y,z) do { if(NULL==(x=(z)malloc(y))) { winerror("Error reserving memory"); exit(-1); } memset(x,0,y); } while(0) -#define _(x) x + // Function declarations + static void WINAPI service_main(DWORD argc, LPTSTR* argv); + static void WINAPI handler(DWORD code); + static int service_install(); + static int service_uninstall(); + extern int hermes_main(int argc, char** argv); -/** - * The docs on microsoft's web don't seem very clear, so I have - * looked at the stunnel source code to understand how this thing - * works. What you see here is still original source, but is - * "inspired" by stunnel's source code (gui.c mainly). - * It's the real minimum needed to install, start and stop services - */ + // Helper class for managing command line parameters + class Parameters { + public: + Parameters() { + params_ = std::make_unique(2); + params_[0] = new char[1]; + params_[0][0] = '\0'; + params_[1] = new char[1024](); + } -extern bool quit; + ~Parameters() { + delete[] params_[0]; + delete[] params_[1]; + } -extern int hermes_main(int,char**); + char** get() { return params_.get(); } + char* operator[](size_t index) { return params_[index]; } -SERVICE_STATUS service_status; -SERVICE_STATUS_HANDLE service_status_handle; - -int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int); -static void WINAPI service_main(DWORD,LPTSTR*); -static void WINAPI handler(DWORD); -static int service_install(); -static int service_uninstall(); - -char **params=NULL; -#define free_params() \ - do \ - { \ - if(NULL!=params) \ - { \ - condfree(params[0]); \ - condfree(params[1]); \ - } \ - condfree(params); \ - } \ - while(0) - -#define init_params() \ - do \ - { \ - free_params(); \ - safemalloc(params,sizeof(char *)*2,char **); \ - safemalloc(params[0],1*sizeof(char),char *); \ - params[0][0]='\0'; \ - safemalloc(params[1],1024*sizeof(char),char *); \ - } \ - while(0) - -int WINAPI WinMain(HINSTANCE instance,HINSTANCE previous_instance,LPSTR cmdline,int cmdshow) -{ - if(!cmp(cmdline,"-service")) - { - SERVICE_TABLE_ENTRY service_table[]={ - {SERVICE_SHORT_NAME,service_main}, - {NULL,NULL} + private: + std::unique_ptr params_; }; - if(0==StartServiceCtrlDispatcher(service_table)) - { - winerror("Error starting service dispatcher."); - return -1; + // Helper functions + void show_error(const char* message) { + MessageBox(NULL, message, ("Error from " SERVICE_NAME), MB_OK | MB_ICONERROR); } - } - else if(!cmp(cmdline,"-install")) - service_install(); - else if(!cmp(cmdline,"-uninstall")) - service_uninstall(); - else - { - //we know that hermes can only have one parameter, so - //just copy it - init_params(); - strncpy(params[1],cmdline,1024); - hermes_main(2,(char **)params); - free_params(); - } - return 0; + void show_message(const char* message) { + MessageBox(NULL, message, ("Message from " SERVICE_NAME), MB_OK | MB_ICONINFORMATION); + } + + void update_service_status(DWORD new_state) { + service_status.dwCurrentState = new_state; + SetServiceStatus(service_status_handle, &service_status); + } } -static int service_install() -{ - SC_HANDLE scm,service_handle; - SERVICE_DESCRIPTION service_description; - char filename[1024]; - string exepath; +int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous_instance, LPSTR cmdline, int cmdshow) { + try { + if (strncmp(cmdline, "-service", strlen("-service")) == 0) { + SERVICE_TABLE_ENTRY service_table[] = { + {const_cast(SERVICE_SHORT_NAME), service_main}, + {NULL, NULL} + }; - if(NULL==(scm=OpenSCManager(NULL,NULL,SC_MANAGER_CREATE_SERVICE))) - { - winerror(_("Error opening connection to the Service Manager.")); - exit(-1); - } - if(0==GetModuleFileName(NULL,filename,sizeof(filename))) - { - winerror(_("Error getting the file name of the process.")); - exit(-1); - } + if (!StartServiceCtrlDispatcher(service_table)) { + throw std::runtime_error("Error starting service dispatcher"); + } + } + else if (strncmp(cmdline, "-install", strlen("-install")) == 0) { + return service_install(); + } + else if (strncmp(cmdline, "-uninstall", strlen("-uninstall")) == 0) { + return service_uninstall(); + } + else { + Parameters params; + strncpy(params[1], cmdline, 1023); + params[1][1023] = '\0'; // Ensure null termination + return hermes_main(2, params.get()); + } + } + catch (const std::exception& e) { + show_error(e.what()); + return -1; + } - exepath=string("\"")+filename+"\" -service"; - - service_handle=CreateService( - scm, //scm handle - SERVICE_SHORT_NAME, //service name - SERVICE_NAME, //display name - SERVICE_ALL_ACCESS, //desired access - SERVICE_WIN32_OWN_PROCESS, //service type - SERVICE_AUTO_START, //start type - SERVICE_ERROR_NORMAL, //error control - exepath.c_str(), //executable path with arguments - NULL, //load group - NULL, //tag for group id - NULL, //dependencies - NULL, //user name - NULL); //password - - if(NULL==service_handle) - { - winerror("Error creating service. Already installed?"); - exit(-1); - } - else - winmessage("Service successfully installed."); - - //createservice doesn't have a field for description - //so we use ChangeServiceConfig2 - service_description.lpDescription=SERVICE_DESCRIPTION_TEXT; - ChangeServiceConfig2(service_handle,SERVICE_CONFIG_DESCRIPTION,(void *)&service_description); - - CloseServiceHandle(service_handle); - CloseServiceHandle(scm); - - return 0; + return 0; } -static int service_uninstall() -{ - SC_HANDLE scm,service_handle; - SERVICE_STATUS status; +static int service_install() { + char filename[1024] = {0}; + if (GetModuleFileName(NULL, filename, sizeof(filename)) == 0) { + throw std::runtime_error("Error getting the file name of the process"); + } - if(NULL==(scm=OpenSCManager(NULL,NULL,SC_MANAGER_CREATE_SERVICE))) - { - winerror(_("Error opening connection to the Service Manager.")); - exit(-1); - } + SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE); + if (!scm) { + throw std::runtime_error("Error opening connection to the Service Manager"); + } - if(NULL==(service_handle=OpenService(scm,SERVICE_SHORT_NAME,SERVICE_QUERY_STATUS|DELETE))) - { - winerror(_("Error opening service.")); - CloseServiceHandle(scm); - exit(-1); - } + std::string exepath = "\"" + std::string(filename) + "\" -service"; + + SC_HANDLE service_handle = CreateService( + scm, // SCM handle + SERVICE_SHORT_NAME, // Service name + SERVICE_NAME, // Display name + SERVICE_ALL_ACCESS, // Desired access + SERVICE_WIN32_OWN_PROCESS, // Service type + SERVICE_AUTO_START, // Start type + SERVICE_ERROR_NORMAL, // Error control + exepath.c_str(), // Executable path + NULL, NULL, NULL, NULL, NULL // Other parameters + ); + + if (!service_handle) { + CloseServiceHandle(scm); + throw std::runtime_error("Error creating service. Already installed?"); + } + + // Set service description + SERVICE_DESCRIPTION service_description = {const_cast(SERVICE_DESCRIPTION_TEXT)}; + ChangeServiceConfig2(service_handle, SERVICE_CONFIG_DESCRIPTION, &service_description); - if(0==QueryServiceStatus(service_handle,&status)) - { - winerror(_("Error querying service.")); - CloseServiceHandle(scm); CloseServiceHandle(service_handle); - exit(-1); - } - - if(SERVICE_STOPPED!=status.dwCurrentState) - { - winerror(SERVICE_NAME _(" is still running. Stop it before trying to uninstall it.")); CloseServiceHandle(scm); - CloseServiceHandle(service_handle); - exit(-1); - } + show_message("Service successfully installed"); + return 0; +} - if(0==DeleteService(service_handle)) - { - winerror(_("Error deleting service.")); +static int service_uninstall() { + SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE); + if (!scm) { + throw std::runtime_error("Error opening connection to the Service Manager"); + } + + SC_HANDLE service_handle = OpenService(scm, SERVICE_SHORT_NAME, SERVICE_QUERY_STATUS | DELETE); + if (!service_handle) { + CloseServiceHandle(scm); + throw std::runtime_error("Error opening service"); + } + + SERVICE_STATUS status; + if (!QueryServiceStatus(service_handle, &status)) { + CloseServiceHandle(service_handle); + CloseServiceHandle(scm); + throw std::runtime_error("Error querying service"); + } + + if (status.dwCurrentState != SERVICE_STOPPED) { + CloseServiceHandle(service_handle); + CloseServiceHandle(scm); + throw std::runtime_error(SERVICE_NAME " is still running. Stop it before trying to uninstall it"); + } + + if (!DeleteService(service_handle)) { + CloseServiceHandle(service_handle); + CloseServiceHandle(scm); + throw std::runtime_error("Error deleting service"); + } + + CloseServiceHandle(service_handle); CloseServiceHandle(scm); - CloseServiceHandle(service_handle); - exit(-1); - } - - CloseServiceHandle(scm); - CloseServiceHandle(service_handle); - winmessage(_("Service successfully uninstalled.")); - return 0; + show_message("Service successfully uninstalled"); + return 0; } -static void WINAPI service_main(DWORD argc,LPTSTR *argv) -{ - char *tmpstr; +static void WINAPI service_main(DWORD argc, LPTSTR* argv) { + service_status = { + .dwServiceType = SERVICE_WIN32, + .dwControlsAccepted = SERVICE_ACCEPT_STOP, + .dwWin32ExitCode = NO_ERROR, + .dwServiceSpecificExitCode = NO_ERROR, + .dwCheckPoint = 0, + .dwWaitHint = 0 + }; - //configure service_status structure - service_status.dwServiceType=SERVICE_WIN32; - service_status.dwControlsAccepted=0; - service_status.dwWin32ExitCode=NO_ERROR; - service_status.dwServiceSpecificExitCode=NO_ERROR; - service_status.dwCheckPoint=0; - service_status.dwWaitHint=0; - service_status.dwControlsAccepted|=SERVICE_ACCEPT_STOP; + service_status_handle = RegisterServiceCtrlHandler(SERVICE_SHORT_NAME, handler); + if (!service_status_handle) { + return; + } - service_status_handle=RegisterServiceCtrlHandler(SERVICE_SHORT_NAME,handler); + update_service_status(SERVICE_RUNNING); - if(0!=service_status_handle) - { - //set service status - ChangeServiceStatus(service_status_handle,service_status,SERVICE_RUNNING); + try { + Parameters params; + if (GetModuleFileName(NULL, params[1], 1024) == 0) { + throw std::runtime_error("Error getting module filename"); + } - //get the path to the config file - init_params(); - GetModuleFileName(NULL,params[1],1024); - if(NULL==(tmpstr=strrchr(params[1],'\\'))) { winerror("Error finding default config file."); exit(-1); } - *(++tmpstr)='\0'; - strncat(params[1],"hermes.ini",strlen("hermes.ini")); + char* config_path = strrchr(params[1], '\\'); + if (!config_path) { + throw std::runtime_error("Error finding default config file"); + } - //now start our main program - hermes_main(2,(char **)params); + *(++config_path) = '\0'; + strncat(params[1], "hermes.ini", strlen("hermes.ini")); - free_params(); + hermes_main(2, params.get()); - //when we are here, we have been stopped - ChangeServiceStatus(service_status_handle,service_status,SERVICE_STOP_PENDING); - ChangeServiceStatus(service_status_handle,service_status,SERVICE_STOPPED); - } + update_service_status(SERVICE_STOP_PENDING); + update_service_status(SERVICE_STOPPED); + } + catch (const std::exception& e) { + show_error(e.what()); + update_service_status(SERVICE_STOPPED); + } } -static void WINAPI handler(DWORD code) -{ - if(SERVICE_CONTROL_STOP==code) - { - quit=true; - ChangeServiceStatus(service_status_handle,service_status,SERVICE_STOP_PENDING); - } +static void WINAPI handler(DWORD code) { + if (code == SERVICE_CONTROL_STOP) { + quit = true; + update_service_status(SERVICE_STOP_PENDING); + } } From d3f59c0fb3837dc633389c29ce1275a2e767e96b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Guti=C3=A9rrez?= Date: Fri, 28 Mar 2025 17:30:16 +0100 Subject: [PATCH 09/12] Add Boost development package as build dependency --- .drone.yml | 5 ++--- Dockerfile | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.drone.yml b/.drone.yml index 665b314..1e7b285 100644 --- a/.drone.yml +++ b/.drone.yml @@ -15,7 +15,7 @@ steps: - name: build hermes image: alpine commands: - - apk add -t hermes-build-deps --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 + - apk add -t hermes-build-deps --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 boost-dev - cmake -B build_dir -D BUILD_DOCS=ON - cmake --build build_dir - name: docker image build @@ -41,7 +41,7 @@ steps: - name: build hermes image: alpine commands: - - apk add -t hermes-build-deps --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 + - apk add -t hermes-build-deps --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 boost-dev - cmake -B build_dir - cmake --build build_dir - name: docker image build @@ -73,4 +73,3 @@ steps: `${DRONE_REPO}` build #${DRONE_BUILD_NUMBER} status: **${DRONE_BUILD_STATUS}** [${DRONE_BUILD_LINK}] - diff --git a/Dockerfile b/Dockerfile index 9d3c1dc..9ecde43 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM alpine:3.18 ADD . /hermes WORKDIR /hermes -RUN apk add --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 +RUN apk add --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 boost-dev RUN cmake -B build RUN cmake --build build RUN mkdir /hermes-installation From 8171f9666aee4bee82ccd7e3a1829b309dcd2981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Guti=C3=A9rrez?= Date: Fri, 28 Mar 2025 17:35:13 +0100 Subject: [PATCH 10/12] Add fmt library as dependency Add fmt-dev to build dependencies and fmt to runtime dependencies in Dockerfile and drone pipeline. This modern C++ formatting library will be used for improved string formatting and logging. --- .drone.yml | 4 ++-- Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.drone.yml b/.drone.yml index 1e7b285..c340edb 100644 --- a/.drone.yml +++ b/.drone.yml @@ -15,7 +15,7 @@ steps: - name: build hermes image: alpine commands: - - apk add -t hermes-build-deps --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 boost-dev + - apk add -t hermes-build-deps --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 boost-dev fmt-dev - cmake -B build_dir -D BUILD_DOCS=ON - cmake --build build_dir - name: docker image build @@ -41,7 +41,7 @@ steps: - name: build hermes image: alpine commands: - - apk add -t hermes-build-deps --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 boost-dev + - apk add -t hermes-build-deps --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 boost-dev fmt-dev - cmake -B build_dir - cmake --build build_dir - name: docker image build diff --git a/Dockerfile b/Dockerfile index 9ecde43..f343e6b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM alpine:3.18 ADD . /hermes WORKDIR /hermes -RUN apk add --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 boost-dev +RUN apk add --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 boost-dev fmt-dev RUN cmake -B build RUN cmake --build build RUN mkdir /hermes-installation @@ -10,5 +10,5 @@ RUN cmake --install build --prefix /hermes-installation FROM alpine:3.18 EXPOSE 25 COPY --from=0 /hermes-installation /hermes -RUN apk add --no-cache openssl libspf2 sqlite-libs libstdc++ libgcc +RUN apk add --no-cache openssl libspf2 sqlite-libs libstdc++ libgcc fmt CMD ["/hermes/bin/hermes", "/config/hermesrc"] From 71d682560371331219979324208c5bd440fc86dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Guti=C3=A9rrez?= Date: Fri, 28 Mar 2025 17:38:11 +0100 Subject: [PATCH 11/12] fix --- .drone.yml | 8 ++++---- Dockerfile | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.drone.yml b/.drone.yml index c340edb..67bc366 100644 --- a/.drone.yml +++ b/.drone.yml @@ -9,11 +9,11 @@ platform: steps: - name: prepare workspace - image: alpine + image: alpine:3.21 commands: - rm -fr build_dir - name: build hermes - image: alpine + image: alpine:3.21 commands: - apk add -t hermes-build-deps --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 boost-dev fmt-dev - cmake -B build_dir -D BUILD_DOCS=ON @@ -35,11 +35,11 @@ platform: steps: - name: prepare workspace - image: alpine + image: alpine:3.21 commands: - rm -fr build_dir - name: build hermes - image: alpine + image: alpine:3.21 commands: - apk add -t hermes-build-deps --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 boost-dev fmt-dev - cmake -B build_dir diff --git a/Dockerfile b/Dockerfile index f343e6b..e4a1771 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,14 @@ -FROM alpine:3.18 +FROM alpine:3.21 ADD . /hermes WORKDIR /hermes -RUN apk add --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 boost-dev fmt-dev +RUN apk add --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 boost-dev fmt RUN cmake -B build RUN cmake --build build RUN mkdir /hermes-installation RUN cmake --install build --prefix /hermes-installation -FROM alpine:3.18 +FROM alpine:3.21 EXPOSE 25 COPY --from=0 /hermes-installation /hermes -RUN apk add --no-cache openssl libspf2 sqlite-libs libstdc++ libgcc fmt +RUN apk add --no-cache openssl libspf2 sqlite-libs libstdc++ libgcc CMD ["/hermes/bin/hermes", "/config/hermesrc"] From cda002ad913256d777d6c503fb2382686841e462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Guti=C3=A9rrez?= Date: Fri, 28 Mar 2025 17:42:47 +0100 Subject: [PATCH 12/12] add fmt dependency --- CMakeLists.txt | 5 ++++- Dockerfile | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 272d3ec..4237637 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,9 @@ target_compile_definitions(hermes PRIVATE LOGGER_CLASS=${LOGGER_CLASS}) target_sources(hermes PRIVATE src/${LOGGER_CLASS}.cpp) # required dependency sqlite3 -find_library (SQLITE3_LIBRARY NAMES libsqlite3 sqlite3) +find_library (SQLITE3_LIBRARY REQUIRED NAMES libsqlite3 sqlite3) + +find_library (FMT_LIBRARY REQUIRED NAMES fmt) # optional dependency libspf2 find_library (SPF2_LIBRARY REQUIRED NAMES spf2 libspf2) @@ -93,6 +95,7 @@ target_link_libraries(hermes ${SQLITE3_LIBRARY} ${OPENSSL_LIBRARIES} ${SPF2_LIBRARY} + ${FMT_LIBRARY} pthread) install(TARGETS hermes diff --git a/Dockerfile b/Dockerfile index e4a1771..d141f33 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM alpine:3.21 ADD . /hermes WORKDIR /hermes -RUN apk add --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 boost-dev fmt +RUN apk add --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 boost-dev fmt-dev RUN cmake -B build RUN cmake --build build RUN mkdir /hermes-installation