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..f4117c6 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,7 +53,7 @@ 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) 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/include/Configfile.h b/include/Configfile.h new file mode 100644 index 0000000..fa6c7c7 --- /dev/null +++ b/include/Configfile.h @@ -0,0 +1,144 @@ +/** + * 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 CONFIGFILE_H +#define CONFIGFILE_H + +#include "hermes.h" +#include +#include +#include +#include + +#include "Utils.h" + +using namespace std; + +class Configfile +{ + private: + static string parseAsString(string); + static bool parseAsBool(string); + static long parseAsInt(string); + static list parseAsList(string); + int uid; + int gid; + bool background; +string chroot; +bool drop_privileges; +string user; +string group; +string pid_file; +int listening_port; +string bind_to; +string server_host; +int server_port; +string database_file; +bool greylist; +bool throttle; +int throttling_time; +int number_of_unimplemented_commands_allowed; +bool allow_data_before_banner; +list dns_blacklist_domains; +int dns_blacklist_percentage; +list dns_whitelist_domains; +int dns_whitelist_percentage; +bool add_status_header; +int banner_delay_time; +int initial_expiry; +int initial_blacklist; +int whitelist_expiry; +bool submit_stats; +bool submit_stats_ssl; +string submit_stats_username; +string submit_stats_password; +int log_level; +string file_logger_filename; +bool keep_file_locked; +int log_rotation_frequency; +string rotate_filename; +bool clean_db; +bool outgoing_ssl; +bool incoming_ssl; +string private_key_file; +string certificate_file; +string dhparams_file; +bool add_headers; +string hostname; +bool whitelisted_disables_everything; +bool reject_no_reverse_resolution; +bool check_helo_against_reverse; +bool query_spf; +bool return_temp_error_on_reject; + public: + Configfile(); + void parse(string); + void validateConfig(); + int getUid(); + int getGid(); + bool& getBackground(); +string& getChroot(); +bool& getDropPrivileges(); +string& getUser(); +string& getGroup(); +string& getPidFile(); +int& getListeningPort(); +string& getBindTo(); +string& getServerHost(); +int& getServerPort(); +string& getDatabaseFile(); +bool& getGreylist(); +bool& getThrottle(); +int& getThrottlingTime(); +int& getNumberOfUnimplementedCommandsAllowed(); +bool& getAllowDataBeforeBanner(); +list& getDnsBlacklistDomains(); +int& getDnsBlacklistPercentage(); +list& getDnsWhitelistDomains(); +int& getDnsWhitelistPercentage(); +bool& getAddStatusHeader(); +int& getBannerDelayTime(); +int& getInitialExpiry(); +int& getInitialBlacklist(); +int& getWhitelistExpiry(); +bool& getSubmitStats(); +bool& getSubmitStatsSsl(); +string& getSubmitStatsUsername(); +string& getSubmitStatsPassword(); +int& getLogLevel(); +string& getFileLoggerFilename(); +bool& getKeepFileLocked(); +int& getLogRotationFrequency(); +string& getRotateFilename(); +bool& getCleanDb(); +bool& getOutgoingSsl(); +bool& getIncomingSsl(); +string& getPrivateKeyFile(); +string& getCertificateFile(); +string& getDhparamsFile(); +bool& getAddHeaders(); +string& getHostname(); +bool& getWhitelistedDisablesEverything(); +bool& getRejectNoReverseResolution(); +bool& getCheckHeloAgainstReverse(); +bool& getQuerySpf(); +bool& getReturnTempErrorOnReject(); +}; + +#endif //CONFIGFILE_H 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/Configfile.cpp b/src/Configfile.cpp new file mode 100644 index 0000000..14900bd --- /dev/null +++ b/src/Configfile.cpp @@ -0,0 +1,314 @@ +/** + * hermes antispam proxy + * Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * @author Juan José Gutiérrez de Quevedo + */ +#include "Configfile.h" + +#include + +extern LOGGER_CLASS hermes_log; + +/** + * default config + * + */ +Configfile::Configfile() +{ +background=true; +chroot=""; +drop_privileges=true; +user="nobody"; +group="nobody"; +pid_file="/var/run/hermes.pid"; +listening_port=25; +bind_to=""; +server_host="localhost"; +server_port=2525; +database_file="/var/hermes/greylisting.db"; +greylist=true; +throttle=true; +throttling_time=1; +number_of_unimplemented_commands_allowed=-1; +allow_data_before_banner=false; +dns_blacklist_domains=Configfile::parseAsList(""); +dns_blacklist_percentage=100; +dns_whitelist_domains=Configfile::parseAsList(""); +dns_whitelist_percentage=100; +add_status_header=false; +banner_delay_time=5; +initial_expiry=240; +initial_blacklist=5; +whitelist_expiry=36; +submit_stats=true; +submit_stats_ssl=true; +submit_stats_username="anonymous"; +submit_stats_password="anonymous"; +log_level=1; +file_logger_filename="hermes.log"; +keep_file_locked=true; +log_rotation_frequency=1440; +rotate_filename="hermes-%%year%%-%%month%%-%%day%%-%%hour%%:%%minute%%.log"; +clean_db=true; +outgoing_ssl=false; +incoming_ssl=false; +private_key_file="/etc/hermes/hermes.key"; +certificate_file="/etc/hermes/hermes.cert"; +dhparams_file=""; +add_headers=true; +hostname=""; +whitelisted_disables_everything=true; +reject_no_reverse_resolution=false; +check_helo_against_reverse=false; +query_spf=false; +return_temp_error_on_reject=false; +} + +void Configfile::parse(string file) +{ + ifstream f; + char line[255]; + int equalpos; + + LINF("parsing "+Utils::get_canonical_filename(file)+" configuration file"); + f.open(file.c_str(),ios::in); + while(!f.eof()) + { + f.getline(line,255); + string l=Utils::trim(line); + if('#'!=l[0]&&l!=""&&l.find("=")) + { + equalpos=l.find("="); + string option=Utils::trim(l.substr(0,equalpos)); + string value=Utils::trim(l.substr(equalpos+1)); + //if(false==cfg.getBackground()) + // cout << option << " -> " << value << endl; + + //this is a bit of a hack, but simplifies a lot this function + #define PARSE_INT(x,y) if(x==option) y=Configfile::parseAsInt(value); else + #define PARSE_BOOL(x,y) if(x==option) y=Configfile::parseAsBool(value); else + #define PARSE_STRING(x,y) if(x==option) y=Configfile::parseAsString(value); else + #define PARSE_LIST(x,y) if(x==option) y=Configfile::parseAsList(value); else + + PARSE_BOOL("background",background) +PARSE_STRING("chroot",chroot) +PARSE_BOOL("drop_privileges",drop_privileges) +PARSE_STRING("user",user) +PARSE_STRING("group",group) +PARSE_STRING("pid_file",pid_file) +PARSE_INT("listening_port",listening_port) +PARSE_STRING("bind_to",bind_to) +PARSE_STRING("server_host",server_host) +PARSE_INT("server_port",server_port) +PARSE_STRING("database_file",database_file) +PARSE_BOOL("greylist",greylist) +PARSE_BOOL("throttle",throttle) +PARSE_INT("throttling_time",throttling_time) +PARSE_INT("number_of_unimplemented_commands_allowed",number_of_unimplemented_commands_allowed) +PARSE_BOOL("allow_data_before_banner",allow_data_before_banner) +PARSE_LIST("dns_blacklist_domains",dns_blacklist_domains) +PARSE_INT("dns_blacklist_percentage",dns_blacklist_percentage) +PARSE_LIST("dns_whitelist_domains",dns_whitelist_domains) +PARSE_INT("dns_whitelist_percentage",dns_whitelist_percentage) +PARSE_BOOL("add_status_header",add_status_header) +PARSE_INT("banner_delay_time",banner_delay_time) +PARSE_INT("initial_expiry",initial_expiry) +PARSE_INT("initial_blacklist",initial_blacklist) +PARSE_INT("whitelist_expiry",whitelist_expiry) +PARSE_BOOL("submit_stats",submit_stats) +PARSE_BOOL("submit_stats_ssl",submit_stats_ssl) +PARSE_STRING("submit_stats_username",submit_stats_username) +PARSE_STRING("submit_stats_password",submit_stats_password) +PARSE_INT("log_level",log_level) +PARSE_STRING("file_logger_filename",file_logger_filename) +PARSE_BOOL("keep_file_locked",keep_file_locked) +PARSE_INT("log_rotation_frequency",log_rotation_frequency) +PARSE_STRING("rotate_filename",rotate_filename) +PARSE_BOOL("clean_db",clean_db) +PARSE_BOOL("outgoing_ssl",outgoing_ssl) +PARSE_BOOL("incoming_ssl",incoming_ssl) +PARSE_STRING("private_key_file",private_key_file) +PARSE_STRING("certificate_file",certificate_file) +PARSE_STRING("dhparams_file",dhparams_file) +PARSE_BOOL("add_headers",add_headers) +PARSE_STRING("hostname",hostname) +PARSE_BOOL("whitelisted_disables_everything",whitelisted_disables_everything) +PARSE_BOOL("reject_no_reverse_resolution",reject_no_reverse_resolution) +PARSE_BOOL("check_helo_against_reverse",check_helo_against_reverse) +PARSE_BOOL("query_spf",query_spf) +PARSE_BOOL("return_temp_error_on_reject",return_temp_error_on_reject) + { + throw Exception("Option \""+option+"\" with value \""+value+"\" is not recognized",__FILE__,__LINE__); + } + #undef PARSE_INT + #undef PARSE_BOOL + #undef PARSE_STRING + #undef PARSE_LIST + } + } + #ifndef WIN32 + uid=Utils::usertouid(user); + gid=Utils::grouptogid(group); + #endif //WIN32 + f.close(); +} + +//again, this is a BIG HACK, but it simplifies code a lot +#define GET_VAR(x,y,z) z Configfile::x(){ return y;} + +GET_VAR(getUid,uid,int) +GET_VAR(getGid,gid,int) +GET_VAR(getBackground,background,bool&) +GET_VAR(getChroot,chroot,string&) +GET_VAR(getDropPrivileges,drop_privileges,bool&) +GET_VAR(getUser,user,string&) +GET_VAR(getGroup,group,string&) +GET_VAR(getPidFile,pid_file,string&) +GET_VAR(getListeningPort,listening_port,int&) +GET_VAR(getBindTo,bind_to,string&) +GET_VAR(getServerHost,server_host,string&) +GET_VAR(getServerPort,server_port,int&) +GET_VAR(getDatabaseFile,database_file,string&) +GET_VAR(getGreylist,greylist,bool&) +GET_VAR(getThrottle,throttle,bool&) +GET_VAR(getThrottlingTime,throttling_time,int&) +GET_VAR(getNumberOfUnimplementedCommandsAllowed,number_of_unimplemented_commands_allowed,int&) +GET_VAR(getAllowDataBeforeBanner,allow_data_before_banner,bool&) +GET_VAR(getDnsBlacklistDomains,dns_blacklist_domains,list&) +GET_VAR(getDnsBlacklistPercentage,dns_blacklist_percentage,int&) +GET_VAR(getDnsWhitelistDomains,dns_whitelist_domains,list&) +GET_VAR(getDnsWhitelistPercentage,dns_whitelist_percentage,int&) +GET_VAR(getAddStatusHeader,add_status_header,bool&) +GET_VAR(getBannerDelayTime,banner_delay_time,int&) +GET_VAR(getInitialExpiry,initial_expiry,int&) +GET_VAR(getInitialBlacklist,initial_blacklist,int&) +GET_VAR(getWhitelistExpiry,whitelist_expiry,int&) +GET_VAR(getSubmitStats,submit_stats,bool&) +GET_VAR(getSubmitStatsSsl,submit_stats_ssl,bool&) +GET_VAR(getSubmitStatsUsername,submit_stats_username,string&) +GET_VAR(getSubmitStatsPassword,submit_stats_password,string&) +GET_VAR(getLogLevel,log_level,int&) +GET_VAR(getFileLoggerFilename,file_logger_filename,string&) +GET_VAR(getKeepFileLocked,keep_file_locked,bool&) +GET_VAR(getLogRotationFrequency,log_rotation_frequency,int&) +GET_VAR(getRotateFilename,rotate_filename,string&) +GET_VAR(getCleanDb,clean_db,bool&) +GET_VAR(getOutgoingSsl,outgoing_ssl,bool&) +GET_VAR(getIncomingSsl,incoming_ssl,bool&) +GET_VAR(getPrivateKeyFile,private_key_file,string&) +GET_VAR(getCertificateFile,certificate_file,string&) +GET_VAR(getDhparamsFile,dhparams_file,string&) +GET_VAR(getAddHeaders,add_headers,bool&) +GET_VAR(getHostname,hostname,string&) +GET_VAR(getWhitelistedDisablesEverything,whitelisted_disables_everything,bool&) +GET_VAR(getRejectNoReverseResolution,reject_no_reverse_resolution,bool&) +GET_VAR(getCheckHeloAgainstReverse,check_helo_against_reverse,bool&) +GET_VAR(getQuerySpf,query_spf,bool&) +GET_VAR(getReturnTempErrorOnReject,return_temp_error_on_reject,bool&) + +#undef GET_VAR + +void Configfile::validateConfig() +{ + #ifndef WIN32 + //check if we are root if we want to bind to a port lower than 1024 + if(getuid()!=0&&listening_port<1024) + throw Exception(_("You can't bind to a port lower than 1024 without being root"),__FILE__,__LINE__); + #endif //WIN32 + + #ifdef HAVE_SSL + //check if ssl is usable + if(!Utils::file_exists(certificate_file)) + throw Exception("Certificate file "+certificate_file+" doesn't exist.\nTo generate a certificate look in hermesrc.example, there is an example there.",__FILE__,__LINE__); + + if(!Utils::file_exists(private_key_file)) + throw Exception("Private key file "+private_key_file+" doesn't exist.\nTo generate a private key look in hermesrc.example, there is an example there.",__FILE__,__LINE__); + #endif //HAVE_SSL + + #ifndef WIN32 + //check if chroot dir exist //TODO: check that files needed in chroot exist + //for now only /etc/resolv.conf, but we're working on it :-D + if(""!=chroot&&!Utils::dir_exists(chroot)) + throw Exception("Directory "+chroot+" doesn't exist, can't chroot to it.",__FILE__,__LINE__); + #endif //WIN32 + + //check if we have submit_stats on but no user and password + if(getSubmitStats()&&(""==getSubmitStatsUsername()||""==getSubmitStatsPassword())) + throw Exception("You have configured hermes to send stats, but have not configured a username or password.\n" + "If you don't have one, go to http://www.hermes-project.com and register there",__FILE__,__LINE__); + + #ifndef HAVE_SSL + //check if we have activated submit_stats_ssl not having ssl activated + if(getSubmitStatsSsl()) + throw Exception("You have configured stats submission through SSL, but hermes was compiled without SSL support",__FILE__,__LINE__); + #endif //HAVE_SSL + +} + +string Configfile::parseAsString(string str) +{ + //remove "" round the string + if('"'==str[0]) + str=str.substr(1); + if('"'==str[str.length()-1]) + str=str.substr(0,str.length()-1); + + return str; +} + +bool Configfile::parseAsBool(string str) +{ + if("yes"==str||"on"==str||"1"==str||"true"==str) + return true; + else + return false; +} + +long int Configfile::parseAsInt(string str) +{ + long int value; + + errno=0; //to know why we do this, read NOTES on strtol(3) + value=strtol(str.c_str(),NULL,10); + if(errno) + throw Exception("Error parsing as int ("+Utils::errnotostrerror(errno)+")",__FILE__,__LINE__); + + return value; +} + +list Configfile::parseAsList(string str) +{ + list tmpList; + string::size_type startpos=0,endpos=0,len; + string tmpstr; + + str=Configfile::parseAsString(str); //remove quotes around string + + len=str.length(); + while(startpos - * - * 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(); +}