Compare commits
5 commits
7735c201d3
...
a4dbaef334
Author | SHA1 | Date | |
---|---|---|---|
a4dbaef334 | |||
47854a3a88 | |||
95298f0e1f | |||
e548847f43 | |||
a4c68baa45 |
30 changed files with 1228 additions and 703 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
build/
|
||||
docs/
|
||||
hermesrc.example
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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 <cru@zodia.de>
|
||||
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 <juanjo@gutierrezdequevedo.com> 1.4
|
||||
- removed patches, they are now on upstream
|
||||
|
||||
* Fri May 25 2007 Veit Wahlich <cru@zodia.de> 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 <cru@zodia.de> 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 <juanjo@gutierrezdequevedo.com>
|
||||
- Fixed rpm to create /var/hermes
|
||||
|
||||
* Fri Apr 11 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
|
||||
- Initial release
|
144
include/Configfile.h
Normal file
144
include/Configfile.h
Normal file
|
@ -0,0 +1,144 @@
|
|||
/**
|
||||
* hermes antispam proxy
|
||||
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; version 2 of the License
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
|
||||
*/
|
||||
#ifndef CONFIGFILE_H
|
||||
#define CONFIGFILE_H
|
||||
|
||||
#include "hermes.h"
|
||||
#include <list>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
|
||||
#include "Utils.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
class Configfile
|
||||
{
|
||||
private:
|
||||
static string parseAsString(string);
|
||||
static bool parseAsBool(string);
|
||||
static long parseAsInt(string);
|
||||
static list<string> 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<string> dns_blacklist_domains;
|
||||
int dns_blacklist_percentage;
|
||||
list<string> 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<string>& getDnsBlacklistDomains();
|
||||
int& getDnsBlacklistPercentage();
|
||||
list<string>& 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
|
19
include/Proxy.h
Normal file
19
include/Proxy.h
Normal file
|
@ -0,0 +1,19 @@
|
|||
// Proxy.h
|
||||
#pragma once
|
||||
#include <string>
|
||||
#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;
|
||||
};
|
16
include/SocketInterface.h
Normal file
16
include/SocketInterface.h
Normal file
|
@ -0,0 +1,16 @@
|
|||
// SocketInterface.h
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
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;
|
||||
};
|
|
@ -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
|
|
@ -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("",<HTMLIN>);
|
||||
close HTMLIN;
|
||||
|
||||
while(<>)
|
||||
{
|
||||
chomp;
|
||||
if(! /^#/ && ! /^\t*$/ && ! /^\*/)
|
||||
{
|
||||
s/^\s+//;s/\s+$//;
|
||||
@_=split ",";
|
||||
my $camelcased=&camel_case($_[1]);
|
||||
my $type=$_[0];
|
||||
$type="list<string>" 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("",<CPPIN>);
|
||||
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("",<HIN>);
|
||||
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<length($str);$i++)
|
||||
{
|
||||
my $letter=substr($str,$i,1);
|
||||
if($letter eq "_")
|
||||
{
|
||||
$i++;
|
||||
$outstr.=uc(substr($str,$i,1));
|
||||
}
|
||||
else
|
||||
{
|
||||
$outstr.=$letter;
|
||||
}
|
||||
}
|
||||
return ucfirst($outstr);
|
||||
}
|
112
scripts/generate_config.py
Normal file
112
scripts/generate_config.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import re
|
||||
import string
|
||||
|
||||
def camel_case(str_):
|
||||
"""Convert snake_case to CamelCase."""
|
||||
return string.capwords(str_, "_").replace("_", "")
|
||||
|
||||
def main():
|
||||
# Read input files
|
||||
with open('../docs/hermes-options.html.in', 'r') as f:
|
||||
html_templ = f.read()
|
||||
|
||||
hvar1 = ""
|
||||
hvar2 = ""
|
||||
cppvar1 = ""
|
||||
cppvar2 = ""
|
||||
cppvar3 = ""
|
||||
conf_example = ""
|
||||
htmlvar = ""
|
||||
htmlexpl = ""
|
||||
|
||||
# Process input
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
|
||||
if not line or line.startswith('#') or line.startswith('*'):
|
||||
if line == '*clean*':
|
||||
htmlexpl = ""
|
||||
elif line.startswith('*'):
|
||||
line = line.replace('*', '#')
|
||||
conf_example += line + "\n"
|
||||
|
||||
# Convert line for HTML
|
||||
line_html = line.lstrip('#').replace('>', '>')
|
||||
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<string>'
|
||||
|
||||
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()
|
314
src/Configfile.cpp
Normal file
314
src/Configfile.cpp
Normal file
|
@ -0,0 +1,314 @@
|
|||
/**
|
||||
* hermes antispam proxy
|
||||
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; version 2 of the License
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
|
||||
*/
|
||||
#include "Configfile.h"
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
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<string>&)
|
||||
GET_VAR(getDnsBlacklistPercentage,dns_blacklist_percentage,int&)
|
||||
GET_VAR(getDnsWhitelistDomains,dns_whitelist_domains,list<string>&)
|
||||
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<string> Configfile::parseAsList(string str)
|
||||
{
|
||||
list<string> tmpList;
|
||||
string::size_type startpos=0,endpos=0,len;
|
||||
string tmpstr;
|
||||
|
||||
str=Configfile::parseAsString(str); //remove quotes around string
|
||||
|
||||
len=str.length();
|
||||
while(startpos<len&&string::npos!=endpos)
|
||||
{
|
||||
endpos=str.find(',',startpos);
|
||||
if(string::npos==endpos)
|
||||
tmpstr=str.substr(startpos);
|
||||
else
|
||||
tmpstr=str.substr(startpos,endpos-startpos);
|
||||
tmpList.push_back(Utils::trim(tmpstr));
|
||||
startpos=endpos+1;
|
||||
}
|
||||
|
||||
return tmpList;
|
||||
}
|
537
src/Proxy.cpp
537
src/Proxy.cpp
|
@ -1,338 +1,233 @@
|
|||
/**
|
||||
* hermes antispam proxy
|
||||
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; version 2 of the License
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
|
||||
*/
|
||||
// Proxy.cpp
|
||||
#include "Proxy.h"
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include "Utils.h" // Dummy include; replace with actual Utils implementation
|
||||
#include "Configfile.h" // Dummy include; replace with actual Configfile implementation
|
||||
|
||||
extern 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<SMTP_STATE_WAIT_FOR_DATA)
|
||||
LINF("421 (probably-throttling) from "+(""==from?"no-from":from)+" (ip:"+peer_address+", hostname:"+(""==resolvedname?"not-resolved":resolvedname)+", ehlo:"+(""==ehlostr?"no-ehlo":ehlostr)+") -> to "+(""==to?"no-to":to));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
61
src/Proxy.h
61
src/Proxy.h
|
@ -1,61 +0,0 @@
|
|||
/**
|
||||
* hermes antispam proxy
|
||||
* Copyright (C) 2006, 2007 Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; version 2 of the License
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
|
||||
*/
|
||||
#ifndef PROXY_H
|
||||
#define PROXY_H
|
||||
|
||||
#include "hermes.h"
|
||||
#include <sys/param.h>
|
||||
#ifdef WIN32
|
||||
#include <winsock2.h>
|
||||
#else
|
||||
#include <sys/select.h>
|
||||
#endif
|
||||
#include <sys/time.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <iostream>
|
||||
|
||||
#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
|
51
src/SocketInterface.cpp
Normal file
51
src/SocketInterface.cpp
Normal file
|
@ -0,0 +1,51 @@
|
|||
// BoostSocket.h
|
||||
#include <boost/asio.hpp>
|
||||
#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_;
|
||||
};
|
117
src/Spf.cpp
117
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 <juanjo@gutierrezdequevedo.com>
|
||||
*/
|
||||
|
||||
#include "Spf.h"
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
|
||||
SPF_server_t *Spf::spfserver=NULL;
|
||||
SPF_server_t *Spf::spfserver = nullptr;
|
||||
|
||||
/**
|
||||
* constructor
|
||||
* 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
|
||||
* 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
|
||||
* Destructor
|
||||
*
|
||||
* frees the memory of the spfrequest
|
||||
* 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;
|
||||
}
|
||||
|
|
14
test/CMakeLists.txt
Normal file
14
test/CMakeLists.txt
Normal file
|
@ -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)
|
96
test/mocks/libspf2_mock.h
Normal file
96
test/mocks/libspf2_mock.h
Normal file
|
@ -0,0 +1,96 @@
|
|||
#ifndef LIBSPF2_MOCK_H
|
||||
#define LIBSPF2_MOCK_H
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <string>
|
||||
#include <spf2/spf.h>
|
||||
|
||||
// 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
|
15
test/mocks/socket_mock.h
Normal file
15
test/mocks/socket_mock.h
Normal file
|
@ -0,0 +1,15 @@
|
|||
// MockSocket.h
|
||||
#include "SocketInterface.h"
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
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));
|
||||
};
|
63
test/tests/proxy_test.cpp
Normal file
63
test/tests/proxy_test.cpp
Normal file
|
@ -0,0 +1,63 @@
|
|||
// ProxyTest.cpp
|
||||
#include <gtest/gtest.h>
|
||||
#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:<test@example.com>"));
|
||||
EXPECT_CALL(mock_socket, writeLine("MAIL FROM:<test@example.com>"));
|
||||
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:<test@example.com>"))
|
||||
.WillOnce(testing::Return("RCPT TO:<receiver@example.com>"))
|
||||
.WillOnce(testing::Return(""));
|
||||
|
||||
EXPECT_CALL(mock_socket, writeLine("MAIL FROM:<test@example.com>"));
|
||||
EXPECT_CALL(mock_socket, writeLine("RCPT TO:<receiver@example.com>"));
|
||||
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();
|
||||
}
|
98
test/tests/spf_test.cpp
Normal file
98
test/tests/spf_test.cpp
Normal file
|
@ -0,0 +1,98 @@
|
|||
#include <gtest/gtest.h>
|
||||
#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();
|
||||
}
|
Loading…
Add table
Reference in a new issue