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/CMakeLists.txt b/CMakeLists.txt index 257ad0e..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) @@ -52,9 +53,9 @@ include_directories( 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} | - ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_config.pl + 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.pl) + docs/hermes-options.html.in scripts/generate_config.py) # doxygen 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/include/Proxy.h b/include/Proxy.h new file mode 100644 index 0000000..7e266c0 --- /dev/null +++ b/include/Proxy.h @@ -0,0 +1,19 @@ +// Proxy.h +#pragma once +#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) {} + + void run(std::string& peer_address); + +private: + SocketInterface* outside; +}; 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/include/SocketInterface.h b/include/SocketInterface.h new file mode 100644 index 0000000..bce1566 --- /dev/null +++ b/include/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/Spf.h b/include/Spf.h similarity index 95% rename from src/Spf.h rename to include/Spf.h index 9e1a07f..ce968fd 100644 --- a/src/Spf.h +++ b/include/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/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/scripts/generate_config.pl b/scripts/generate_config.pl deleted file mode 100755 index 0c2faf0..0000000 --- a/scripts/generate_config.pl +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/perl -w - -# this small script generates the Configfile class from the -# Configfile.cpp.in and Configfile.h.in. this way when we want -# to add a new option to the config file, we just have to put it -# on Configfile.tmpl and automagically it will appear on our code -# It will also generate an example hermesrc from the same info. -# 2007-04-17 Now it also generates an html document for our webpage - -my $hvar=""; -my $cppvar1="",$cppvar2="",$cppvar3="",$conf_example="",$htmlvar=""; - -open HTMLIN, "<../docs/hermes-options.html.in"; -$htmltempl=join("",); -close HTMLIN; - -while(<>) -{ - chomp; - if(! /^#/ && ! /^\t*$/ && ! /^\*/) - { - s/^\s+//;s/\s+$//; - @_=split ","; - my $camelcased=&camel_case($_[1]); - my $type=$_[0]; - $type="list" if($type =~ /list/); - $hvar1.="$type $_[1];\n"; - $hvar2.="$type& get$camelcased();\n"; - if($type =~ /list/) - { - $cppvar1.="$_[1]=Configfile::parseAsList($_[2]);\n"; - } - else - { - $cppvar1.="$_[1]=$_[2];\n"; - } - $cppvar2.="PARSE_".uc($_[0])."(\"$_[1]\",$_[1])\n"; - $cppvar3.="GET_VAR(get$camelcased,$_[1],$type&)\n"; - $conf_example.="$_[1] = $_[2]\n\n"; - my $htmltemp=$htmltempl; - $htmltemp =~ s/%type%/$_[0]/; - $htmltemp =~ s/%name%/$_[1]/g; - $htmltemp =~ s/%default%/$_[2]/; - $htmltemp =~ s/%explanation%/$htmlexpl/; - $htmlexpl=""; - $htmlvar.=$htmltemp; - } - else - { - if(/^\*clean\*$/) # clean restarts our htmlexpl contents - { - $htmlexpl=""; - } - else - { - if(/^\*/) - { - s/^\*$//; - s/^\*/#/; - $conf_example.="$_\n"; - chomp; - s/^#//; - s/>/>/; - $htmlexpl.="$_\n"; - } - } - } -} - -chomp $cppvar1; -chomp $cppvar2; -chomp $cppvar3; -chomp $hvar1; -chomp $hvar2; -chomp $conf_example; - -open CPPIN, "<../src/Configfile.cpp.in"; -$cppstr=join("",); -close CPPIN; -open CPPOUT, ">Configfile.cpp"; -$cppstr =~ s/%templ_default_values%/$cppvar1/; -$cppstr =~ s/%templ_parsevars%/$cppvar2/; -$cppstr =~ s/%templ_getmethods%/$cppvar3/; -print CPPOUT $cppstr; -close CPPOUT; - -open HIN, "<../src/Configfile.h.in"; -$hstr=join("",); -close HIN; -open HOUT, ">Configfile.h"; -$hstr =~ s/%templ_privateattribs%/$hvar1/; -$hstr =~ s/%templ_publicmethods%/$hvar2/; -print HOUT $hstr; -close HOUT; - -open RCEX, ">../dists/hermesrc.example"; -print RCEX $conf_example; -close RCEX; - -open HTML, ">../docs/hermes-options.html"; -print HTML $htmlvar; -close HTML; - -sub camel_case() -{ - my $str=shift; - my $outstr=""; - - for($i=0;$i', '>') + htmlexpl += line_html + "\n" + continue + + parts = line.split(',') + parts = [p.strip() for p in parts] + + type_str = parts[0] + var_name = parts[1] + default_val = parts[2] + + # Modify type for lists + if 'list' in type_str: + type_str = 'list' + + camel_name = camel_case(var_name) + + # Generate header variables + hvar1 += f"{type_str} {var_name};\n" + hvar2 += f"{type_str}& get{camel_name}();\n" + + # Generate cpp variables + if 'list' in type_str: + cppvar1 += f"{var_name} = Configfile::parseAsList({default_val});\n" + else: + cppvar1 += f"{var_name} = {default_val};\n" + + cppvar2 += f"PARSE_{parts[0].upper()}(\"{var_name}\", {var_name})\n" + cppvar3 += f"GET_VAR(get{camel_name}, {var_name}, {type_str}&)\n" + + # Generate config example + conf_example += f"{var_name} = {default_val}\n\n" + + # Generate HTML + html_temp = html_templ.replace('%type%', parts[0]) \ + .replace('%name%', var_name) \ + .replace('%default%', default_val) \ + .replace('%explanation%', htmlexpl) + htmlvar += html_temp + htmlexpl = "" + + # Clean up variables + for var in [cppvar1, cppvar2, cppvar3, hvar1, hvar2, conf_example]: + var = var.rstrip() + + # Read and write Configfile.cpp + with open('../src/Configfile.cpp.in', 'r') as f: + cpp_str = f.read() + + cpp_str = cpp_str.replace('%templ_default_values%', cppvar1) \ + .replace('%templ_parsevars%', cppvar2) \ + .replace('%templ_getmethods%', cppvar3) + + with open('Configfile.cpp', 'w') as f: + f.write(cpp_str) + + # 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) + + # Write hermesrc.example + with open('../dists/hermesrc.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 __name__ == "__main__": + main() 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; - } } diff --git a/src/Proxy.h b/src/Proxy.h deleted file mode 100644 index fd55da2..0000000 --- a/src/Proxy.h +++ /dev/null @@ -1,61 +0,0 @@ -/** - * 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 - -#include "hermes.h" -#include -#ifdef WIN32 - #include -#else - #include -#endif -#include -#include -#include -#include -#include -#include - -#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&); -}; - -#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/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/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/test/mocks/libspf2_mock.h b/test/mocks/libspf2_mock.h new file mode 100644 index 0000000..09d61b9 --- /dev/null +++ b/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/test/mocks/socket_mock.h b/test/mocks/socket_mock.h new file mode 100644 index 0000000..63e3819 --- /dev/null +++ b/test/mocks/socket_mock.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/test/tests/proxy_test.cpp b/test/tests/proxy_test.cpp new file mode 100644 index 0000000..7a06603 --- /dev/null +++ b/test/tests/proxy_test.cpp @@ -0,0 +1,63 @@ +// ProxyTest.cpp +#include +#include "Proxy.h" +#include "socket_mock.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(); +} diff --git a/test/tests/spf_test.cpp b/test/tests/spf_test.cpp new file mode 100644 index 0000000..b205875 --- /dev/null +++ b/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(); +}