Compare commits
12 commits
Author | SHA1 | Date | |
---|---|---|---|
|
cda002ad91 | ||
|
71d6825603 | ||
|
8171f9666a | ||
|
d3f59c0fb3 | ||
|
975c5ad5df | ||
b4b995dd19 | |||
|
3c8bd791e6 | ||
b05c4ad5d9 | |||
d7e2aee3a3 | |||
518f578298 | |||
49f3fd3a6b | |||
d2a90e4cd1 |
33 changed files with 1102 additions and 988 deletions
13
.drone.yml
13
.drone.yml
|
@ -9,13 +9,13 @@ platform:
|
|||
|
||||
steps:
|
||||
- name: prepare workspace
|
||||
image: alpine
|
||||
image: alpine:3.21
|
||||
commands:
|
||||
- rm -fr build_dir
|
||||
- name: build hermes
|
||||
image: alpine
|
||||
image: alpine:3.21
|
||||
commands:
|
||||
- apk add -t hermes-build-deps --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3
|
||||
- apk add -t hermes-build-deps --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 boost-dev fmt-dev
|
||||
- cmake -B build_dir -D BUILD_DOCS=ON
|
||||
- cmake --build build_dir
|
||||
- name: docker image build
|
||||
|
@ -35,13 +35,13 @@ platform:
|
|||
|
||||
steps:
|
||||
- name: prepare workspace
|
||||
image: alpine
|
||||
image: alpine:3.21
|
||||
commands:
|
||||
- rm -fr build_dir
|
||||
- name: build hermes
|
||||
image: alpine
|
||||
image: alpine:3.21
|
||||
commands:
|
||||
- apk add -t hermes-build-deps --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3
|
||||
- apk add -t hermes-build-deps --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 boost-dev fmt-dev
|
||||
- cmake -B build_dir
|
||||
- cmake --build build_dir
|
||||
- name: docker image build
|
||||
|
@ -73,4 +73,3 @@ steps:
|
|||
`${DRONE_REPO}` build #${DRONE_BUILD_NUMBER} status: **${DRONE_BUILD_STATUS}**
|
||||
|
||||
[${DRONE_BUILD_LINK}]
|
||||
|
||||
|
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
build/
|
||||
docs/
|
||||
hermesrc.example
|
|
@ -6,6 +6,7 @@ set(LOGGER_CLASS UnixLogger CACHE STRING "One of UnixLogger, FileLogger or NullL
|
|||
|
||||
add_executable (hermes
|
||||
${CMAKE_CURRENT_BINARY_DIR}/Configfile.cpp
|
||||
${CMAKE_CURRENT_BINARY_DIR}/Configfile.h
|
||||
src/Exception.cpp
|
||||
src/hermes.cpp
|
||||
src/ServerSocket.cpp
|
||||
|
@ -14,7 +15,7 @@ add_executable (hermes
|
|||
src/Proxy.cpp
|
||||
src/Socket.cpp)
|
||||
|
||||
option(BUILD_DOC "Build documentation")
|
||||
option(BUILD_DOCS "Build documentation")
|
||||
|
||||
if(WIN32)
|
||||
set(SOURCES ${SOURCES}
|
||||
|
@ -23,14 +24,17 @@ 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)
|
||||
|
||||
# required dependency sqlite3
|
||||
find_library (SQLITE3_LIBRARY NAMES libsqlite3 sqlite3)
|
||||
find_library (SQLITE3_LIBRARY REQUIRED NAMES libsqlite3 sqlite3)
|
||||
|
||||
find_library (FMT_LIBRARY REQUIRED NAMES fmt)
|
||||
|
||||
# optional dependency libspf2
|
||||
find_library (SPF2_LIBRARY NAMES spf2 libspf2)
|
||||
find_library (SPF2_LIBRARY REQUIRED NAMES spf2 libspf2)
|
||||
if(SPF2_LIB)
|
||||
target_compile_definitions(hermes PRIVATE HAVE_SPF2)
|
||||
target_sources(hermes PRIVATE src/Spf.cpp)
|
||||
|
@ -48,32 +52,50 @@ include_directories(
|
|||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
src)
|
||||
|
||||
set(CONFIG_TEMPLATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Configfile.tmpl)
|
||||
|
||||
# generation of various files
|
||||
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Configfile.cpp
|
||||
COMMAND cpp ${OPT_DEFS} ${CMAKE_CURRENT_SOURCE_DIR}/src/Configfile.tmpl -I
|
||||
${CMAKE_CURRENT_BINARY_DIR} |
|
||||
python ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_config.py
|
||||
DEPENDS src/Configfile.cpp.in src/Configfile.h.in src/Configfile.tmpl
|
||||
docs/hermes-options.html.in scripts/generate_config.py)
|
||||
COMMAND python "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_config.py" "${CONFIG_TEMPLATE}"
|
||||
--cpp-template "${CMAKE_CURRENT_SOURCE_DIR}/src/Configfile.cpp.in"
|
||||
--output-cpp "${CMAKE_CURRENT_BINARY_DIR}/Configfile.cpp"
|
||||
DEPENDS ${CONFIG_TEMPLATE} src/Configfile.cpp.in)
|
||||
|
||||
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Configfile.h
|
||||
COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_config.py "${CONFIG_TEMPLATE}"
|
||||
--h-template "${CMAKE_CURRENT_SOURCE_DIR}/include/Configfile.h.in"
|
||||
--output-h "${CMAKE_CURRENT_BINARY_DIR}/Configfile.h"
|
||||
DEPENDS ${CONFIG_TEMPLATE} include/Configfile.h.in)
|
||||
|
||||
# DEPENDS src/Configfile.cpp.in src/Configfile.h.in src/Configfile.tmpl
|
||||
# docs/hermes-options.html.in scripts/generate_config.py)
|
||||
|
||||
# doxygen
|
||||
if (BUILD_DOCS)
|
||||
add_custom_command(OUTPUT docs/hermesrc.example
|
||||
COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_config.py "${CONFIG_TEMPLATE}"
|
||||
--output-example "${CMAKE_CURRENT_BINARY_DIR}/docs/hermesrc.example"
|
||||
DEPENDS ${CONFIG_TEMPLATE})
|
||||
add_custom_command(OUTPUT docs/html/hermes-options.html
|
||||
COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_config.py "${CONFIG_TEMPLATE}"
|
||||
--html-template "${CMAKE_CURRENT_SOURCE_DIR}/docs/hermes-options.html.in"
|
||||
--output-html "${CMAKE_CURRENT_BINARY_DIR}/docs/html/hermes-options.html"
|
||||
DEPENDS ${CONFIG_TEMPLATE} docs/hermes-options.html.in)
|
||||
find_package (Doxygen)
|
||||
if(DOXYGEN_FOUND)
|
||||
add_custom_target(doc ALL
|
||||
doxygen
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/docs)
|
||||
install(
|
||||
DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/docs/html
|
||||
TYPE DOC)
|
||||
endif()
|
||||
add_custom_target(doc ALL
|
||||
doxygen "${CMAKE_CURRENT_SOURCE_DIR}/docs/Doxyfile"
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/docs
|
||||
DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/docs/html/hermes-options.html" "${CMAKE_CURRENT_BINARY_DIR}/docs/hermesrc.example")
|
||||
install(
|
||||
DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/docs/html
|
||||
TYPE DOC)
|
||||
endif()
|
||||
|
||||
target_link_libraries(hermes
|
||||
${SQLITE3_LIBRARY}
|
||||
${OPENSSL_LIBRARIES}
|
||||
${SPF2_LIBRARY}
|
||||
${FMT_LIBRARY}
|
||||
pthread)
|
||||
|
||||
install(TARGETS hermes
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
FROM alpine:3.18
|
||||
FROM alpine:3.21
|
||||
ADD . /hermes
|
||||
WORKDIR /hermes
|
||||
RUN apk add --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3
|
||||
RUN apk add --no-cache graphviz doxygen gcc make openssl-dev libspf2-dev cmake g++ sqlite-dev gettext-dev python3 boost-dev fmt-dev
|
||||
RUN cmake -B build
|
||||
RUN cmake --build build
|
||||
RUN mkdir /hermes-installation
|
||||
RUN cmake --install build --prefix /hermes-installation
|
||||
|
||||
FROM alpine:3.18
|
||||
FROM alpine:3.21
|
||||
EXPOSE 25
|
||||
COPY --from=0 /hermes-installation /hermes
|
||||
RUN apk add --no-cache openssl libspf2 sqlite-libs libstdc++ libgcc
|
||||
|
|
|
@ -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
|
23
include/Proxy.h
Normal file
23
include/Proxy.h
Normal file
|
@ -0,0 +1,23 @@
|
|||
// Proxy.h
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include "Socket.h"
|
||||
|
||||
#define SMTP_STATE_WAIT_FOR_HELO 0
|
||||
#define SMTP_STATE_WAIT_FOR_MAILFROM 1
|
||||
#define SMTP_STATE_WAIT_FOR_RCPTTO 2
|
||||
#define SMTP_STATE_WAIT_FOR_DATA 3
|
||||
|
||||
class Proxy {
|
||||
public:
|
||||
Proxy();
|
||||
void setOutside(Socket& socket);
|
||||
void run(const std::string& peer_address);
|
||||
|
||||
private:
|
||||
boost::asio::io_context io_context_;
|
||||
boost::asio::ssl::context ssl_context;
|
||||
Socket* outside_socket_;
|
||||
};
|
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
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
#include "hermes.h"
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
#include <dirent.h>
|
||||
|
@ -35,7 +36,9 @@
|
|||
#include "Database.h"
|
||||
#include "Socket.h"
|
||||
|
||||
using namespace std;
|
||||
using std::string;
|
||||
using std::stringstream;
|
||||
using std::list;
|
||||
|
||||
#ifdef WIN32
|
||||
#define sleep(x) Sleep(1000*(x))
|
||||
|
@ -52,37 +55,37 @@ class Utils
|
|||
{
|
||||
public:
|
||||
//string utilities
|
||||
static string strtolower(string);
|
||||
static string trim(string);
|
||||
static string strtolower(std::string_view);
|
||||
static string trim(std::string_view);
|
||||
static string inttostr(int);
|
||||
static string ulongtostr(unsigned long);
|
||||
|
||||
//email-related utilities
|
||||
static string getmail(string&);
|
||||
static string getdomain(string&);
|
||||
static string reverseip(string&);
|
||||
static string getmail(const string&);
|
||||
static string getdomain(const string&);
|
||||
static string reverseip(const string&);
|
||||
|
||||
//spam-related utilities (TODO: move to a different class)
|
||||
static bool greylist(string,string&,string&,string&);
|
||||
static bool listed_on_dns_lists(list<string>&,unsigned char,string&);
|
||||
static bool whitelisted(string,string&);
|
||||
static bool blacklisted(string,string&,string&);
|
||||
static bool greylist(const string& dbfile, string& ip, string& p_from, string& p_to);
|
||||
static bool listed_on_dns_lists(const list<string>& dns_domains, unsigned char percentage, const string& ip);
|
||||
static bool whitelisted(const string& dbfile, string& ip);
|
||||
static bool blacklisted(const string& dbfile, string& ip, string& to);
|
||||
|
||||
#ifndef WIN32
|
||||
//posix-utils
|
||||
static int usertouid(string);
|
||||
static int grouptogid(string);
|
||||
static int usertouid(const string& user);
|
||||
static int grouptogid(const string& groupname);
|
||||
#endif //WIN32
|
||||
|
||||
//misc
|
||||
static string get_canonical_filename(string);
|
||||
static bool file_exists(string);
|
||||
static bool dir_exists(string);
|
||||
static string get_canonical_filename(const string& file);
|
||||
static bool file_exists(const string& file);
|
||||
static bool dir_exists(const string& dir);
|
||||
static string errnotostrerror(int);
|
||||
static string rfc2821_date(time_t *timestamp=NULL);
|
||||
static string gethostname();
|
||||
static void write_pid(string,pid_t);
|
||||
static string gethostname(int s);
|
||||
static void write_pid(const string& file, pid_t pid);
|
||||
static string gethostname(int socket);
|
||||
};
|
||||
|
||||
#endif //UTILS_H
|
|
@ -1,37 +1,63 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import re
|
||||
import string
|
||||
import argparse
|
||||
import os
|
||||
|
||||
def camel_case(str_):
|
||||
"""Convert snake_case to CamelCase."""
|
||||
return string.capwords(str_, "_").replace("_", "")
|
||||
|
||||
def main():
|
||||
# Read input files
|
||||
with open('../docs/hermes-options.html.in', 'r') as f:
|
||||
html_templ = f.read()
|
||||
# Set up argument parser
|
||||
parser = argparse.ArgumentParser(description='Generate configuration files for Hermes.')
|
||||
parser.add_argument('input_template',
|
||||
type=argparse.FileType('r'),
|
||||
help='Input configuration template file')
|
||||
parser.add_argument('--html-template',
|
||||
default='',
|
||||
help='Path to HTML template input file')
|
||||
parser.add_argument('--cpp-template',
|
||||
default='',
|
||||
help='Path to C++ template input file')
|
||||
parser.add_argument('--h-template',
|
||||
default='',
|
||||
help='Path to header template input file')
|
||||
parser.add_argument('--output-cpp',
|
||||
default='',
|
||||
help='Output path for generated C++ file')
|
||||
parser.add_argument('--output-h',
|
||||
default='',
|
||||
help='Output path for generated header file')
|
||||
parser.add_argument('--output-example',
|
||||
default='',
|
||||
help='Output path for example configuration')
|
||||
parser.add_argument('--output-html',
|
||||
default='',
|
||||
help='Output path for generated HTML documentation')
|
||||
|
||||
hvar1 = ""
|
||||
hvar2 = ""
|
||||
cppvar1 = ""
|
||||
cppvar2 = ""
|
||||
cppvar3 = ""
|
||||
conf_example = ""
|
||||
htmlvar = ""
|
||||
# Parse arguments
|
||||
args = parser.parse_args()
|
||||
|
||||
hvar1 = []
|
||||
hvar2 = []
|
||||
cppvar1 = []
|
||||
cppvar2 = []
|
||||
cppvar3 = []
|
||||
conf_example = []
|
||||
htmlvar = []
|
||||
htmlexpl = ""
|
||||
|
||||
# Process input
|
||||
for line in sys.stdin:
|
||||
for line in args.input_template:
|
||||
line = line.strip()
|
||||
|
||||
if not line or line.startswith('#') or line.startswith('*'):
|
||||
if line == '*clean*':
|
||||
htmlexpl = ""
|
||||
elif line.startswith('*'):
|
||||
line = line.replace('*', '#')
|
||||
conf_example += line + "\n"
|
||||
line = line.replace('*', '#').strip()
|
||||
conf_example.append(line)
|
||||
|
||||
# Convert line for HTML
|
||||
line_html = line.lstrip('#').replace('>', '>')
|
||||
|
@ -52,61 +78,80 @@ def main():
|
|||
camel_name = camel_case(var_name)
|
||||
|
||||
# Generate header variables
|
||||
hvar1 += f"{type_str} {var_name};\n"
|
||||
hvar2 += f"{type_str}& get{camel_name}();\n"
|
||||
hvar1.append(f"{type_str} {var_name};")
|
||||
hvar2.append(f"{type_str}& get{camel_name}();")
|
||||
|
||||
# Generate cpp variables
|
||||
if 'list' in type_str:
|
||||
cppvar1 += f"{var_name} = Configfile::parseAsList({default_val});\n"
|
||||
cppvar1.append(f"{var_name} = Configfile::parseAsList({default_val});")
|
||||
else:
|
||||
cppvar1 += f"{var_name} = {default_val};\n"
|
||||
cppvar1.append(f"{var_name} = {default_val};")
|
||||
|
||||
cppvar2 += f"PARSE_{parts[0].upper()}(\"{var_name}\", {var_name})\n"
|
||||
cppvar3 += f"GET_VAR(get{camel_name}, {var_name}, {type_str}&)\n"
|
||||
cppvar2.append(f"PARSE_{parts[0].upper()}(\"{var_name}\", {var_name})")
|
||||
cppvar3.append(f"GET_VAR(get{camel_name}, {var_name}, {type_str}&)")
|
||||
|
||||
# Generate config example
|
||||
conf_example += f"{var_name} = {default_val}\n\n"
|
||||
conf_example.append(f"{var_name} = {default_val}")
|
||||
|
||||
# Generate HTML
|
||||
html_temp = html_templ.replace('%type%', parts[0]) \
|
||||
.replace('%name%', var_name) \
|
||||
.replace('%default%', default_val) \
|
||||
.replace('%explanation%', htmlexpl)
|
||||
htmlvar += html_temp
|
||||
if args.html_template:
|
||||
html_templ = open(args.html_template, 'r').read()
|
||||
html_temp = html_templ.replace('%type%', parts[0]) \
|
||||
.replace('%name%', var_name) \
|
||||
.replace('%default%', default_val) \
|
||||
.replace('%explanation%', htmlexpl)
|
||||
htmlvar.append(html_temp)
|
||||
htmlexpl = ""
|
||||
|
||||
# Clean up variables
|
||||
for var in [cppvar1, cppvar2, cppvar3, hvar1, hvar2, conf_example]:
|
||||
var = var.rstrip()
|
||||
# Convert lists to newline-separated strings
|
||||
hvar1 = '\n'.join(hvar1)
|
||||
hvar2 = '\n'.join(hvar2)
|
||||
cppvar1 = '\n'.join(cppvar1)
|
||||
cppvar2 = '\n'.join(cppvar2)
|
||||
cppvar3 = '\n'.join(cppvar3)
|
||||
conf_example = '\n\n'.join(conf_example)
|
||||
htmlvar = ''.join(htmlvar)
|
||||
|
||||
# Read and write Configfile.cpp
|
||||
with open('../src/Configfile.cpp.in', 'r') as f:
|
||||
cpp_str = f.read()
|
||||
# Write Configfile.cpp
|
||||
if args.cpp_template and args.output_cpp:
|
||||
try:
|
||||
cpp_str = open(args.cpp_template, 'r').read()
|
||||
cpp_str = cpp_str.replace('%templ_default_values%', cppvar1) \
|
||||
.replace('%templ_parsevars%', cppvar2) \
|
||||
.replace('%templ_getmethods%', cppvar3)
|
||||
|
||||
cpp_str = cpp_str.replace('%templ_default_values%', cppvar1) \
|
||||
.replace('%templ_parsevars%', cppvar2) \
|
||||
.replace('%templ_getmethods%', cppvar3)
|
||||
os.makedirs(os.path.dirname(args.output_cpp), exist_ok=True)
|
||||
with open(args.output_cpp, 'w') as f:
|
||||
f.write(cpp_str)
|
||||
except FileNotFoundError:
|
||||
print(f"Error: C++ template file {args.cpp_template} not found.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with open('Configfile.cpp', 'w') as f:
|
||||
f.write(cpp_str)
|
||||
# Write Configfile.h
|
||||
if args.h_template and args.output_h:
|
||||
try:
|
||||
h_str = open(args.h_template, 'r').read()
|
||||
h_str = h_str.replace('%templ_privateattribs%', hvar1) \
|
||||
.replace('%templ_publicmethods%', hvar2)
|
||||
|
||||
# Read and write Configfile.h
|
||||
with open('../src/Configfile.h.in', 'r') as f:
|
||||
h_str = f.read()
|
||||
|
||||
h_str = h_str.replace('%templ_privateattribs%', hvar1) \
|
||||
.replace('%templ_publicmethods%', hvar2)
|
||||
|
||||
with open('Configfile.h', 'w') as f:
|
||||
f.write(h_str)
|
||||
os.makedirs(os.path.dirname(args.output_h), exist_ok=True)
|
||||
with open(args.output_h, 'w') as f:
|
||||
f.write(h_str)
|
||||
except FileNotFoundError:
|
||||
print(f"Error: Header template file {args.h_template} not found.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Write hermesrc.example
|
||||
with open('../dists/hermesrc.example', 'w') as f:
|
||||
f.write(conf_example)
|
||||
if args.output_example:
|
||||
os.makedirs(os.path.dirname(args.output_example), exist_ok=True)
|
||||
with open(args.output_example, 'w') as f:
|
||||
f.write(conf_example)
|
||||
|
||||
# Write hermes-options.html
|
||||
with open('../docs/hermes-options.html', 'w') as f:
|
||||
f.write(htmlvar)
|
||||
if args.output_html and htmlvar:
|
||||
os.makedirs(os.path.dirname(args.output_html), exist_ok=True)
|
||||
with open(args.output_html, 'w') as f:
|
||||
f.write(htmlvar)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
*
|
||||
|
||||
*clean*
|
||||
#ifndef WIN32
|
||||
|
||||
* whether to fork to the background. initscripts require
|
||||
* this to be true most of the time.
|
||||
|
@ -35,7 +34,6 @@ string,group,"nobody"
|
|||
* if you set background=true above, this will write the pid
|
||||
* of the forked hermes, not the original.
|
||||
string,pid_file,"/var/run/hermes.pid"
|
||||
#endif //WIN32
|
||||
|
||||
* the port where hermes will listen for new connection.
|
||||
* if you are going to use a port lower than 1024 (almost always,
|
||||
|
@ -58,11 +56,7 @@ int,server_port,2525
|
|||
* database file to use.
|
||||
* if you are chrooting, the path is relative to the chroot:
|
||||
* real filepath = chroot + database_file
|
||||
#ifdef WIN32
|
||||
string,database_file,"greylisting.db"
|
||||
#else
|
||||
string,database_file,"/var/hermes/greylisting.db"
|
||||
#endif //WIN32
|
||||
|
||||
* whether to use greylisting.
|
||||
* greylisting will slightly delay your emails (configurable, see below)
|
||||
|
@ -120,13 +114,6 @@ bool,add_status_header,false
|
|||
* time to delay the initial SMTP banner
|
||||
int,banner_delay_time,5
|
||||
|
||||
#ifdef REALLY_VERBOSE_DEBUG
|
||||
* email to notify exceptions to.
|
||||
* CAVEAT: the code that does this is VERY BUGGY and VERY VERBOSE, don't use unless you
|
||||
* are a developer looking for a bug.
|
||||
string,notify_to,""
|
||||
#endif //REALLY_VERBOSE_DEBUG
|
||||
|
||||
* greylisting options.
|
||||
*
|
||||
*clean*
|
||||
|
@ -142,56 +129,12 @@ int,initial_blacklist,5
|
|||
* 36 is a magic number, is the maximum days between a day and the same day next month
|
||||
int,whitelist_expiry,36
|
||||
|
||||
* whether to submit stats.
|
||||
bool,submit_stats,true
|
||||
|
||||
* should stats be submited using SSL?
|
||||
* recomended, but some people will compile without ssl.
|
||||
#ifdef HAVE_SSL
|
||||
bool,submit_stats_ssl,true
|
||||
#else
|
||||
bool,submit_stats_ssl,false
|
||||
#endif //HAVE_SSL
|
||||
|
||||
* username (used to submit stats).
|
||||
* you can register on http://www.hermes-project.com
|
||||
string,submit_stats_username,"anonymous"
|
||||
|
||||
* password
|
||||
string,submit_stats_password,"anonymous"
|
||||
|
||||
* log level:
|
||||
* 0: log only errors
|
||||
* 1: log errors and information (default)
|
||||
* 2: debug (passwords might be written in plaintext with this option, so use with care)
|
||||
int,log_level,1
|
||||
|
||||
#if LOGGER_CLASS==FileLogger
|
||||
* if you are using the filelogger, which file to log to.
|
||||
string,file_logger_filename,"hermes.log"
|
||||
|
||||
* whether to keep the logger file locked between writes
|
||||
bool,keep_file_locked,true
|
||||
|
||||
* frequency for log rotating in minutes
|
||||
* default is 1440 (1 day)
|
||||
* 0 means no rotation
|
||||
int,log_rotation_frequency,1440
|
||||
|
||||
* format for the logfile rotation
|
||||
* if you are using logfile rotation, file_logger represents the filename
|
||||
* to which the logger will write, while this is the name files will get
|
||||
* when rotated
|
||||
* you can use the following variables:
|
||||
* %%year%% - current year (4 digits)
|
||||
* %%month%% - current month
|
||||
* %%day%% - current day
|
||||
* %%hour%% - current hour
|
||||
* %%minute%% - current minute
|
||||
* all of them are zero-padded
|
||||
string,rotate_filename,"hermes-%%year%%-%%month%%-%%day%%-%%hour%%:%%minute%%.log"
|
||||
#endif //LOGGER_CLASS==FileLogger
|
||||
|
||||
* whether to clean the database file and send stats.
|
||||
* if you have two instances of hermes running (for example one for smtp and other for smtps)
|
||||
* you want to configure all of them but one to use clean_db=false.
|
||||
|
@ -201,9 +144,7 @@ string,rotate_filename,"hermes-%%year%%-%%month%%-%%day%%-%%hour%%:%%minute%%.lo
|
|||
* will be left hanging around without any use.
|
||||
bool,clean_db,true
|
||||
|
||||
#ifdef HAVE_SSL
|
||||
* ssl-related config options
|
||||
* NOTE: this NEEDS the openssl library
|
||||
*
|
||||
*clean*
|
||||
|
||||
|
@ -235,7 +176,6 @@ string,certificate_file,"/etc/hermes/hermes.cert"
|
|||
* # openssl dhparam -out dhparam.pem <numbits>
|
||||
* (replace <numbits> with the number of bits suitable for you, e.G. 1024)
|
||||
string,dhparams_file,""
|
||||
#endif //HAVE_SSL
|
||||
|
||||
* whether to add headers to the email sent or no.
|
||||
* to be rfc compatible this HAS to be true, but if you set to false, no one will know you are using hermes
|
||||
|
@ -262,11 +202,7 @@ bool,check_helo_against_reverse,false
|
|||
|
||||
* whether to query the spf record for the incoming domain.
|
||||
* should help, enable if you have libspf (if you don't, install it and recompile)
|
||||
#ifdef HAVE_SPF
|
||||
bool,query_spf,true
|
||||
#else
|
||||
bool,query_spf,false
|
||||
#endif //HAVE_SPF
|
||||
|
||||
* return temporary error instead of permanent error.
|
||||
* Currently, this only applies to SPF and DNSBL rejected email
|
||||
|
|
599
src/Proxy.cpp
599
src/Proxy.cpp
|
@ -1,338 +1,275 @@
|
|||
/**
|
||||
* 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 <sstream>
|
||||
#include <fmt/format.h>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include "Utils.h"
|
||||
#include "Configfile.h"
|
||||
|
||||
extern LOGGER_CLASS hermes_log;
|
||||
extern Configfile cfg;
|
||||
|
||||
void Proxy::setOutside(Socket& p_outside)
|
||||
{
|
||||
outside=p_outside;
|
||||
namespace {
|
||||
std::string resolveHostname(const std::string& ip_address) {
|
||||
try {
|
||||
boost::asio::io_context io_context;
|
||||
boost::asio::ip::tcp::resolver resolver(io_context);
|
||||
boost::asio::ip::address addr = boost::asio::ip::make_address(ip_address);
|
||||
boost::asio::ip::tcp::endpoint ep(addr, 0);
|
||||
auto results = resolver.resolve(ep.address().to_string(), "");
|
||||
if (results.begin() != results.end()) {
|
||||
return results.begin()->host_name();
|
||||
}
|
||||
} catch (const std::exception&) {}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
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);
|
||||
}
|
||||
catch(Exception &e)
|
||||
{
|
||||
LINF("STARTTLS issued by remote, but enableSSL failed!");
|
||||
LERR(e);
|
||||
outside.writeLine("454 Tried to enable SSL but failed");
|
||||
}
|
||||
#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);
|
||||
}
|
||||
|
||||
if(inside.canRead(0.2)) //server wants to send something to client
|
||||
{
|
||||
strtemp=inside.readLine();
|
||||
if(inside.isClosed())
|
||||
return;
|
||||
string code=strtemp.substr(0,3); //all responses by the server start with a code
|
||||
|
||||
if("354"==code) //354 -> you can start sending data, unthrottle now and read binary-safe
|
||||
{
|
||||
string endofdata="";
|
||||
ssize_t bytes_read=0;
|
||||
char buffer[4097];
|
||||
|
||||
outside.writeLine(strtemp);
|
||||
strtemp="";
|
||||
string ssltls="";
|
||||
#ifdef HAVE_SSL
|
||||
if (outside.is_ssl_enabled())
|
||||
ssltls=" (SSL/TLS)";
|
||||
#endif //HAVE_SSL
|
||||
|
||||
if(cfg.getAddHeaders())
|
||||
{
|
||||
inside.writeLine("Received: from "+ehlostr+" ("+peer_address+")");
|
||||
inside.writeLine(" by "+Utils::gethostname(outside.getFD())+" with "+(esmtp?"ESTMP":"SMTP")+ssltls+" via TCP; "+Utils::rfc2821_date());
|
||||
inside.writeLine("X-Anti-Spam-Proxy: Proxied by Hermes [www.hermes-project.com]");
|
||||
if(cfg.getAddStatusHeader())
|
||||
inside.writeLine("X-Hermes-Status: "+hermes_status);
|
||||
}
|
||||
do
|
||||
{
|
||||
bytes_read=outside.readBytes(buffer,sizeof(buffer)-1);
|
||||
if(bytes_read<1)
|
||||
throw NetworkException("Problem reading DATA contents, recv returned "+Utils::inttostr(bytes_read),__FILE__,__LINE__);
|
||||
buffer[bytes_read]='\0';
|
||||
inside.writeBytes(buffer,bytes_read);
|
||||
if(bytes_read<5)
|
||||
endofdata+=string(buffer);
|
||||
else
|
||||
endofdata=string(buffer+bytes_read-5);
|
||||
if(endofdata.length()>5)
|
||||
endofdata=endofdata.substr(endofdata.length()-5);
|
||||
}
|
||||
while(endofdata!="\r\n.\r\n"/*&&endofdata.length()>3&&endofdata.substr(2)!="\n.\n"&&endofdata.substr(2)!="\r.\r"*/);
|
||||
}
|
||||
|
||||
if("235"==code) //235 -> you are correctly authenticated, unthrottle & authenticate
|
||||
{
|
||||
throttled=false;
|
||||
authenticated=true;
|
||||
hermes_status="authenticated";
|
||||
}
|
||||
if("250-pipelining"==Utils::strtolower(strtemp)||"250-chunking"==Utils::strtolower(strtemp)) //this solves our problems with pipelining-enabled servers
|
||||
strtemp="";
|
||||
|
||||
//this is a special case, we can't just ignore the line if it's the last line (doesn't have the dash after the code)
|
||||
//so we just say we support an imaginary extension (noextension).
|
||||
//caveat: this makes us identificable, so, if you can, configure your smtp server to either don't support pipelining
|
||||
//or to not advertise it as the last capability.
|
||||
if("250 pipelining"==Utils::strtolower(strtemp)||"250 chunking"==Utils::strtolower(strtemp))
|
||||
strtemp="250 x-noextension";
|
||||
|
||||
//try to annoy spammers who send us too many senseless commands by delaying their connection a lot
|
||||
if("502"==code) //502 unimplemented -> count them, if bigger than a certain number, terminate connection
|
||||
{
|
||||
if(cfg.getNumberOfUnimplementedCommandsAllowed()!=-1&&++unimplemented_requests>cfg.getNumberOfUnimplementedCommandsAllowed())
|
||||
{
|
||||
inside.writeLine("QUIT");
|
||||
inside.close(); //close the socket now and leave server alone
|
||||
sleep(60);
|
||||
outside.writeLine("502 Too many unimplemented commands, closing connection");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(strtemp.length())
|
||||
outside.writeLine(strtemp);
|
||||
}
|
||||
|
||||
if(throttled)
|
||||
sleep(cfg.getThrottlingTime()); //we take 1 second between each command to make spammers angry
|
||||
}
|
||||
}
|
||||
catch(Exception &e) //any exception will close both connections
|
||||
{
|
||||
LERR(e);
|
||||
if(last_state<SMTP_STATE_WAIT_FOR_DATA)
|
||||
LINF("421 (probably-throttling) from "+(""==from?"no-from":from)+" (ip:"+peer_address+", hostname:"+(""==resolvedname?"not-resolved":resolvedname)+", ehlo:"+(""==ehlostr?"no-ehlo":ehlostr)+") -> to "+(""==to?"no-to":to));
|
||||
return;
|
||||
}
|
||||
Proxy::Proxy()
|
||||
: io_context_(),
|
||||
ssl_context(boost::asio::ssl::context::tlsv12),
|
||||
outside_socket_(nullptr) {
|
||||
}
|
||||
|
||||
void Proxy::setOutside(Socket& socket) {
|
||||
outside_socket_ = &socket;
|
||||
}
|
||||
|
||||
void Proxy::run(const std::string& peer_address) {
|
||||
if (!outside_socket_) {
|
||||
throw std::runtime_error("Outside socket not set");
|
||||
}
|
||||
|
||||
boost::asio::ssl::stream<boost::asio::ip::tcp::socket> inside(io_context_, ssl_context);
|
||||
// Original comments and variables retained
|
||||
const std::string empty_str;
|
||||
std::string from = empty_str;
|
||||
std::string to = empty_str;
|
||||
std::string ehlostr = empty_str;
|
||||
std::string resolvedname = empty_str;
|
||||
unsigned char last_state = SMTP_STATE_WAIT_FOR_HELO;
|
||||
long unimplemented_requests = 0;
|
||||
|
||||
#ifdef HAVE_SPF
|
||||
SpfChecker spf_checker;
|
||||
#endif
|
||||
|
||||
try {
|
||||
// Resolve hostname using the Boost resolver
|
||||
resolvedname = resolveHostname(peer_address);
|
||||
|
||||
// Configure SSL contexts
|
||||
boost::asio::ssl::context ssl_context(boost::asio::ssl::context::tlsv12);
|
||||
ssl_context.set_verify_mode(boost::asio::ssl::verify_none);
|
||||
|
||||
bool throttled = cfg.getThrottle(); // Start with a throttled connection
|
||||
bool authenticated = false; // Start with a non-authenticated connection
|
||||
bool esmtp = false;
|
||||
std::string strtemp;
|
||||
std::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 (cfg.getWhitelistedDisablesEverything()) {
|
||||
throttled = false;
|
||||
}
|
||||
}
|
||||
|
||||
std::string peer_addr_copy = peer_address;
|
||||
if (cfg.getWhitelistedDisablesEverything() && Utils::whitelisted(cfg.getDatabaseFile(), peer_addr_copy)) {
|
||||
throttled = false;
|
||||
authenticated = true;
|
||||
} else {
|
||||
if (!cfg.getAllowDataBeforeBanner()) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(cfg.getBannerDelayTime()));
|
||||
|
||||
// Check if data is waiting before server banner
|
||||
if (outside_socket_->canRead(0.0)) {
|
||||
std::cout << fmt::format("421 (data_before_banner) (ip:{})\n", peer_address);
|
||||
std::this_thread::sleep_for(std::chrono::seconds(20));
|
||||
|
||||
// Write rejection message
|
||||
std::string rejection_msg = fmt::format("421 Stop sending data before we show you the banner\r\n");
|
||||
outside_socket_->writeBytes(const_cast<char*>(rejection_msg.c_str()), rejection_msg.length());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to the inside server
|
||||
boost::asio::ip::tcp::resolver resolver(io_context_);
|
||||
boost::asio::ip::tcp::resolver::results_type endpoints =
|
||||
resolver.resolve(cfg.getServerHost(), std::to_string(cfg.getServerPort()));
|
||||
|
||||
boost::asio::connect(inside.lowest_layer(), endpoints);
|
||||
|
||||
// SSL setup
|
||||
if (cfg.getOutgoingSsl()) {
|
||||
inside.set_verify_mode(boost::asio::ssl::verify_none);
|
||||
inside.handshake(boost::asio::ssl::stream_base::client);
|
||||
}
|
||||
|
||||
if (cfg.getIncomingSsl()) {
|
||||
#ifdef HAVE_SSL
|
||||
outside_socket_->prepareSSL(true);
|
||||
outside_socket_->startSSL(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Communication buffers
|
||||
char read_buffer[4096];
|
||||
ssize_t bytes_read;
|
||||
|
||||
// Main loop for communication
|
||||
while (!outside_socket_->isClosed() && !inside.lowest_layer().is_open()) {
|
||||
// Check if the client wants to send something to the server
|
||||
if (outside_socket_->canRead(1.0)) {
|
||||
bytes_read = outside_socket_->readBytes(read_buffer, sizeof(read_buffer));
|
||||
if (bytes_read > 0) {
|
||||
strtemp = std::string(read_buffer, bytes_read);
|
||||
|
||||
std::string cmd_lower = Utils::strtolower(strtemp.substr(0, std::min<size_t>(10, strtemp.length())));
|
||||
if (strtemp.length() > 10 && "mail from:" == cmd_lower) {
|
||||
from = std::string(Utils::getmail(strtemp));
|
||||
last_state = SMTP_STATE_WAIT_FOR_RCPTTO;
|
||||
}
|
||||
|
||||
cmd_lower = Utils::strtolower(strtemp.substr(0, std::min<size_t>(4, strtemp.length())));
|
||||
if ("ehlo" == cmd_lower) esmtp = true;
|
||||
|
||||
if (strtemp.length() > 4 && ("ehlo" == cmd_lower ||
|
||||
"helo" == cmd_lower)) {
|
||||
ehlostr = std::string(Utils::trim(strtemp.substr(5)));
|
||||
last_state = SMTP_STATE_WAIT_FOR_MAILFROM;
|
||||
}
|
||||
|
||||
// RCPT TO handling with comprehensive checks
|
||||
cmd_lower = Utils::strtolower(strtemp.substr(0, std::min<size_t>(8, strtemp.length())));
|
||||
if (strtemp.length() > 8 && "rcpt to:" == cmd_lower) {
|
||||
std::string mechanism;
|
||||
std::string message;
|
||||
to = std::string(Utils::getmail(strtemp));
|
||||
|
||||
// Construct log string
|
||||
std::string strlog = fmt::format("from {} (ip:{}, hostname:{}, {} {}) -> to {}",
|
||||
from, peer_address, resolvedname, (esmtp ? "ehlo" : "helo"), ehlostr, to);
|
||||
|
||||
// Greylisting check
|
||||
std::string code = "250";
|
||||
std::string peer_addr_copy = peer_address;
|
||||
std::string from_copy = from;
|
||||
std::string to_copy = to;
|
||||
if (cfg.getGreylist() && !authenticated &&
|
||||
Utils::greylist(cfg.getDatabaseFile(), peer_addr_copy, from_copy, to_copy)) {
|
||||
code = "421";
|
||||
mechanism = "greylist";
|
||||
message = fmt::format("{} Greylisted!! Please try again in a few minutes.", code);
|
||||
std::cout << fmt::format("checking {}\n", mechanism);
|
||||
}
|
||||
// SPF Check
|
||||
#ifdef HAVE_SPF
|
||||
else if (cfg.getQuerySpf() && !authenticated &&
|
||||
!spf_checker.query(peer_address, ehlostr, from)) {
|
||||
code = cfg.getAddStatusHeader() ? "250" :
|
||||
(cfg.getReturnTempErrorOnReject() ? "421" : "550");
|
||||
mechanism = "spf";
|
||||
message = fmt::format("{} You do not seem to be allowed to send email for that particular domain.", code);
|
||||
std::cout << fmt::format("checking {}\n", mechanism);
|
||||
}
|
||||
#endif
|
||||
// Blacklist check
|
||||
else if (!authenticated &&
|
||||
Utils::blacklisted(cfg.getDatabaseFile(), peer_addr_copy, to_copy)) {
|
||||
code = cfg.getReturnTempErrorOnReject() ? "421" : "550";
|
||||
mechanism = "allowed-domain-per-ip";
|
||||
message = fmt::format("{} You do not seem to be allowed to send email to that particular domain from that address.", code);
|
||||
std::cout << fmt::format("checking {}\n", mechanism);
|
||||
}
|
||||
// DNS Blacklist check
|
||||
else if (!cfg.getDnsBlacklistDomains().empty() && !authenticated &&
|
||||
Utils::listed_on_dns_lists(cfg.getDnsBlacklistDomains(),
|
||||
cfg.getDnsBlacklistPercentage(),
|
||||
peer_address)) {
|
||||
code = cfg.getAddStatusHeader() ? "250" :
|
||||
(cfg.getReturnTempErrorOnReject() ? "421" : "550");
|
||||
mechanism = "dnsbl";
|
||||
message = fmt::format("{} You are listed on some DNS blacklists. Get delisted before trying to send us email.", code);
|
||||
std::cout << fmt::format("checking {}\n", mechanism);
|
||||
}
|
||||
// Reverse DNS check
|
||||
else if (cfg.getRejectNoReverseResolution() && !authenticated && resolvedname.empty()) {
|
||||
code = cfg.getReturnTempErrorOnReject() ? "421" : "550";
|
||||
mechanism = "no reverse resolution";
|
||||
message = fmt::format("{} Your IP address does not resolve to a hostname.", code);
|
||||
std::cout << fmt::format("checking {}\n", mechanism);
|
||||
}
|
||||
// HELO/Reverse name check
|
||||
else if (cfg.getCheckHeloAgainstReverse() && !authenticated && ehlostr != resolvedname) {
|
||||
code = cfg.getReturnTempErrorOnReject() ? "421" : "550";
|
||||
mechanism = "helo differs from resolved name";
|
||||
message = fmt::format("{} Your IP hostname doesn't match your envelope hostname.", code);
|
||||
std::cout << fmt::format("checking {}\n", mechanism);
|
||||
}
|
||||
|
||||
// Prepare log message
|
||||
std::string log_str = strlog;
|
||||
if (!mechanism.empty()) {
|
||||
log_str = fmt::format("({}) {}", mechanism, log_str);
|
||||
}
|
||||
std::cout << fmt::format("{} {}\n", code, log_str);
|
||||
|
||||
// Handle rejection
|
||||
if (code != "250") {
|
||||
// Close inside connection
|
||||
inside.lowest_layer().close();
|
||||
|
||||
// Delay to annoy spammers
|
||||
std::this_thread::sleep_for(std::chrono::seconds(20));
|
||||
|
||||
// Send rejection message
|
||||
std::string rejection_msg = fmt::format("{}\r\n", message);
|
||||
outside_socket_->writeBytes(const_cast<char*>(rejection_msg.c_str()), rejection_msg.length());
|
||||
return;
|
||||
}
|
||||
|
||||
last_state = SMTP_STATE_WAIT_FOR_DATA;
|
||||
}
|
||||
|
||||
// Send to inside server
|
||||
boost::asio::async_write(inside, boost::asio::buffer(strtemp),
|
||||
[](const boost::system::error_code& /*error*/, std::size_t /*bytes_transferred*/) {});
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the server wants to send something to the client
|
||||
boost::system::error_code ec;
|
||||
size_t server_available = inside.lowest_layer().available(ec);
|
||||
if (!ec && server_available > 0) {
|
||||
std::vector<char> server_buffer(server_available);
|
||||
size_t bytes_read = boost::asio::read(inside, boost::asio::buffer(server_buffer), ec);
|
||||
if (!ec && bytes_read > 0) {
|
||||
outside_socket_->writeBytes(server_buffer.data(), bytes_read);
|
||||
}
|
||||
}
|
||||
|
||||
// Run io_context to process async operations
|
||||
io_context_.poll();
|
||||
|
||||
// Throttling
|
||||
if (throttled) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(cfg.getThrottlingTime()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const boost::system::system_error& e) {
|
||||
std::cerr << fmt::format("Boost.Asio error: {}\n", e.what());
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << fmt::format("Standard exception: {}\n", e.what());
|
||||
}
|
||||
}
|
||||
|
|
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_;
|
||||
};
|
121
src/Spf.cpp
121
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
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,11 @@
|
|||
|
||||
#include <unistd.h>
|
||||
#include <cstring>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
using std::string;
|
||||
using std::stringstream;
|
||||
using std::list;
|
||||
|
||||
extern Configfile cfg;
|
||||
extern LOGGER_CLASS hermes_log;
|
||||
|
@ -67,12 +72,10 @@ string Utils::ulongtostr(unsigned long number)
|
|||
* @return string lowercase version of s
|
||||
*
|
||||
*/
|
||||
string Utils::strtolower(string s)
|
||||
string Utils::strtolower(const std::string_view s)
|
||||
{
|
||||
for(unsigned int i=0;i<s.length();i++)
|
||||
s[i]=tolower(s[i]);
|
||||
|
||||
return s;
|
||||
const std::string str(s);
|
||||
return boost::algorithm::to_lower_copy(str);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,15 +86,15 @@ string Utils::strtolower(string s)
|
|||
* @return string trimmed string
|
||||
*
|
||||
*/
|
||||
string Utils::trim(string s)
|
||||
string Utils::trim(const std::string_view s)
|
||||
{
|
||||
while(isspace(s[0]))
|
||||
s.erase(0,1);
|
||||
|
||||
while(isspace(s[s.length()-1]))
|
||||
s.erase(s.length()-1,1);
|
||||
|
||||
return s;
|
||||
auto start = s.find_first_not_of(" \t\n\r\f\v");
|
||||
if (start == std::string_view::npos) {
|
||||
return string();
|
||||
}
|
||||
|
||||
auto end = s.find_last_not_of(" \t\n\r\f\v");
|
||||
return string(s.substr(start, end - start + 1));
|
||||
}
|
||||
|
||||
//------------------------
|
||||
|
@ -142,7 +145,7 @@ string Utils::trim(string s)
|
|||
* @return whether triplet should get greylisted or not
|
||||
* @todo unify {white,black,grey}list in one function that returns a different constant in each case
|
||||
*/
|
||||
bool Utils::greylist(string dbfile,string& ip,string& p_from,string& p_to)
|
||||
bool Utils::greylist(const string& dbfile, string& ip, string& p_from, string& p_to)
|
||||
{
|
||||
string from=Database::cleanString(p_from);
|
||||
string to=Database::cleanString(p_to);
|
||||
|
@ -181,7 +184,7 @@ bool Utils::greylist(string dbfile,string& ip,string& p_from,string& p_to)
|
|||
* @return whether ip is whitelisted or not
|
||||
* @todo unify {white,black,grey}list in one function that returns a different constant in each case
|
||||
*/
|
||||
bool Utils::whitelisted(string dbfile,string& ip)
|
||||
bool Utils::whitelisted(const string& dbfile, string& ip)
|
||||
{
|
||||
Database db;
|
||||
string hostname;
|
||||
|
@ -213,7 +216,7 @@ bool Utils::whitelisted(string dbfile,string& ip)
|
|||
* @return whether ip is whitelisted or not
|
||||
* @todo this should contain all cases when we should reject a connection
|
||||
*/
|
||||
bool Utils::blacklisted(string dbfile,string& ip,string& to)
|
||||
bool Utils::blacklisted(const string& dbfile, string& ip, string& to)
|
||||
{
|
||||
Database db;
|
||||
string hostname;
|
||||
|
@ -234,7 +237,7 @@ bool Utils::blacklisted(string dbfile,string& ip,string& to)
|
|||
* @return email extracted from rawline
|
||||
*
|
||||
*/
|
||||
string Utils::getmail(string& rawline)
|
||||
string Utils::getmail(const string& rawline)
|
||||
{
|
||||
string email;
|
||||
string::size_type start=0,end=0;
|
||||
|
@ -280,7 +283,7 @@ string Utils::getmail(string& rawline)
|
|||
* @return domain of email
|
||||
*
|
||||
*/
|
||||
string Utils::getdomain(string& email)
|
||||
string Utils::getdomain(const string& email)
|
||||
{
|
||||
if(email.rfind('@'))
|
||||
return trim(email.substr(email.rfind('@')+1));
|
||||
|
@ -307,7 +310,7 @@ string Utils::getdomain(string& email)
|
|||
* @return uid for user
|
||||
*
|
||||
*/
|
||||
int Utils::usertouid(string user)
|
||||
int Utils::usertouid(const string& user)
|
||||
{
|
||||
struct passwd *pwd;
|
||||
pwd=getpwnam(user.c_str());
|
||||
|
@ -330,7 +333,7 @@ int Utils::usertouid(string user)
|
|||
* @return gid for groupname
|
||||
*
|
||||
*/
|
||||
int Utils::grouptogid(string groupname)
|
||||
int Utils::grouptogid(const string& groupname)
|
||||
{
|
||||
struct group *grp;
|
||||
grp=getgrnam(groupname.c_str());
|
||||
|
@ -353,7 +356,7 @@ int Utils::grouptogid(string groupname)
|
|||
* @return is file readable?
|
||||
*
|
||||
*/
|
||||
bool Utils::file_exists(string file)
|
||||
bool Utils::file_exists(const string& file)
|
||||
{
|
||||
FILE *f=fopen(file.c_str(),"r");
|
||||
if(NULL==f)
|
||||
|
@ -366,7 +369,7 @@ bool Utils::file_exists(string file)
|
|||
}
|
||||
|
||||
#ifdef WIN32
|
||||
string Utils::get_canonical_filename(string file)
|
||||
string Utils::get_canonical_filename(const string& file)
|
||||
{
|
||||
char buffer[MAX_PATH];
|
||||
|
||||
|
@ -375,7 +378,7 @@ string Utils::get_canonical_filename(string file)
|
|||
return string(buffer);
|
||||
}
|
||||
#else
|
||||
string Utils::get_canonical_filename(string file)
|
||||
string Utils::get_canonical_filename(const string& file)
|
||||
{
|
||||
char *buffer=NULL;
|
||||
string result;
|
||||
|
@ -387,6 +390,7 @@ string Utils::get_canonical_filename(string file)
|
|||
return result;
|
||||
}
|
||||
#endif //WIN32
|
||||
|
||||
/**
|
||||
* whether a directory is accesible by current process/user
|
||||
*
|
||||
|
@ -395,7 +399,7 @@ string Utils::get_canonical_filename(string file)
|
|||
* @return isdir readable?
|
||||
*
|
||||
*/
|
||||
bool Utils::dir_exists(string dir)
|
||||
bool Utils::dir_exists(const string& dir)
|
||||
{
|
||||
DIR *d=opendir(dir.c_str());
|
||||
if(NULL==d)
|
||||
|
@ -432,8 +436,6 @@ string Utils::errnotostrerror(int errnum)
|
|||
strerr="Error ";
|
||||
#endif //WIN32
|
||||
return string(strerr)+" ("+inttostr(errnum)+")";
|
||||
// else
|
||||
// return string("Error "+inttostr(errno)+" retrieving error code for error number ")+inttostr(errnum);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -459,7 +461,7 @@ string Utils::errnotostrerror(int errnum)
|
|||
*
|
||||
* @return whether ip is blacklisted or not
|
||||
*/
|
||||
bool Utils::listed_on_dns_lists(list<string>& dns_domains,unsigned char percentage,string& ip)
|
||||
bool Utils::listed_on_dns_lists(const list<string>& dns_domains, unsigned char percentage, const string& ip)
|
||||
{
|
||||
string reversedip;
|
||||
unsigned char number_of_lists=dns_domains.size();
|
||||
|
@ -468,7 +470,7 @@ bool Utils::listed_on_dns_lists(list<string>& dns_domains,unsigned char percenta
|
|||
|
||||
reversedip=reverseip(ip);
|
||||
|
||||
for(list<string>::iterator i=dns_domains.begin();i!=dns_domains.end();i++)
|
||||
for(list<string>::const_iterator i=dns_domains.begin();i!=dns_domains.end();i++)
|
||||
{
|
||||
string dns_domain;
|
||||
|
||||
|
@ -515,7 +517,7 @@ bool Utils::listed_on_dns_lists(list<string>& dns_domains,unsigned char percenta
|
|||
*
|
||||
* @return the reversed ip
|
||||
*/
|
||||
string Utils::reverseip(string& ip)
|
||||
string Utils::reverseip(const string& ip)
|
||||
{
|
||||
string inverseip="";
|
||||
string::size_type pos=0,ppos=0;
|
||||
|
@ -629,13 +631,19 @@ string Utils::gethostname()
|
|||
return string(buf);
|
||||
}
|
||||
|
||||
string Utils::gethostname(int s)
|
||||
/**
|
||||
* Get the hostname for a given socket
|
||||
*
|
||||
* @param socket Socket to get hostname for
|
||||
* @return The hostname for the socket
|
||||
*/
|
||||
string Utils::gethostname(int socket)
|
||||
{
|
||||
struct sockaddr_in sa;
|
||||
unsigned int dummy = sizeof sa;
|
||||
struct hostent *hp;
|
||||
|
||||
if (getsockname(s,(struct sockaddr *) &sa,&dummy) == -1)
|
||||
if (getsockname(socket,(struct sockaddr *) &sa,&dummy) == -1)
|
||||
throw Exception("Error getting ip from socket"+Utils::errnotostrerror(errno),__FILE__,__LINE__);
|
||||
|
||||
hp=gethostbyaddr((const void *)&sa.sin_addr,sizeof sa.sin_addr,AF_INET);
|
||||
|
@ -646,8 +654,13 @@ string Utils::gethostname(int s)
|
|||
return string(hp->h_name);
|
||||
}
|
||||
|
||||
|
||||
void Utils::write_pid(string file,pid_t pid)
|
||||
/**
|
||||
* Write process ID to a file
|
||||
*
|
||||
* @param file File to write PID to
|
||||
* @param pid Process ID to write
|
||||
*/
|
||||
void Utils::write_pid(const string& file, pid_t pid)
|
||||
{
|
||||
FILE *f;
|
||||
|
||||
|
@ -658,4 +671,3 @@ void Utils::write_pid(string file,pid_t pid)
|
|||
fprintf(f,"%d\n",pid);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,247 +18,221 @@
|
|||
* @author Juan José Gutiérrez de Quevedo <juanjo@gutierrezdequevedo.com>
|
||||
*/
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <windows.h>
|
||||
|
||||
using namespace std;
|
||||
namespace {
|
||||
// Service configuration constants
|
||||
constexpr const char* SERVICE_NAME = "hermes anti-spam proxy";
|
||||
constexpr const char* SERVICE_SHORT_NAME = "hermes";
|
||||
constexpr const char* SERVICE_DESCRIPTION_TEXT =
|
||||
"An anti-spam proxy using a combination of techniques like greylisting, dnsbl/dnswl, SPF, etc.";
|
||||
|
||||
#define SERVICE_NAME "hermes anti-spam proxy"
|
||||
#define SERVICE_SHORT_NAME "hermes"
|
||||
#define SERVICE_DESCRIPTION_TEXT "An anti-spam proxy using a combination of techniques like greylisting, dnsbl/dnswl, SPF, etc."
|
||||
// Global state
|
||||
SERVICE_STATUS service_status;
|
||||
SERVICE_STATUS_HANDLE service_status_handle;
|
||||
extern bool quit;
|
||||
|
||||
//macros
|
||||
#define ChangeServiceStatus(x,y,z) y.dwCurrentState=z; SetServiceStatus(x,&y);
|
||||
#define MIN(x,y) (((x)<(y))?(x):(y))
|
||||
#define cmp(x,y) strncmp(x,y,strlen(y))
|
||||
#define msgbox(x,y,z) MessageBox(NULL,x,z SERVICE_NAME,MB_OK|y)
|
||||
#define winerror(x) msgbox(x,MB_ICONERROR,"Error from ")
|
||||
#define winmessage(x) msgbox(x,MB_ICONINFORMATION,"Message from ")
|
||||
#define condfree(x) if(NULL!=x) free(x);
|
||||
#define safemalloc(x,y,z) do { if(NULL==(x=(z)malloc(y))) { winerror("Error reserving memory"); exit(-1); } memset(x,0,y); } while(0)
|
||||
#define _(x) x
|
||||
// Function declarations
|
||||
static void WINAPI service_main(DWORD argc, LPTSTR* argv);
|
||||
static void WINAPI handler(DWORD code);
|
||||
static int service_install();
|
||||
static int service_uninstall();
|
||||
extern int hermes_main(int argc, char** argv);
|
||||
|
||||
/**
|
||||
* The docs on microsoft's web don't seem very clear, so I have
|
||||
* looked at the stunnel source code to understand how this thing
|
||||
* works. What you see here is still original source, but is
|
||||
* "inspired" by stunnel's source code (gui.c mainly).
|
||||
* It's the real minimum needed to install, start and stop services
|
||||
*/
|
||||
// Helper class for managing command line parameters
|
||||
class Parameters {
|
||||
public:
|
||||
Parameters() {
|
||||
params_ = std::make_unique<char*[]>(2);
|
||||
params_[0] = new char[1];
|
||||
params_[0][0] = '\0';
|
||||
params_[1] = new char[1024]();
|
||||
}
|
||||
|
||||
extern bool quit;
|
||||
~Parameters() {
|
||||
delete[] params_[0];
|
||||
delete[] params_[1];
|
||||
}
|
||||
|
||||
extern int hermes_main(int,char**);
|
||||
char** get() { return params_.get(); }
|
||||
char* operator[](size_t index) { return params_[index]; }
|
||||
|
||||
SERVICE_STATUS service_status;
|
||||
SERVICE_STATUS_HANDLE service_status_handle;
|
||||
|
||||
int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int);
|
||||
static void WINAPI service_main(DWORD,LPTSTR*);
|
||||
static void WINAPI handler(DWORD);
|
||||
static int service_install();
|
||||
static int service_uninstall();
|
||||
|
||||
char **params=NULL;
|
||||
#define free_params() \
|
||||
do \
|
||||
{ \
|
||||
if(NULL!=params) \
|
||||
{ \
|
||||
condfree(params[0]); \
|
||||
condfree(params[1]); \
|
||||
} \
|
||||
condfree(params); \
|
||||
} \
|
||||
while(0)
|
||||
|
||||
#define init_params() \
|
||||
do \
|
||||
{ \
|
||||
free_params(); \
|
||||
safemalloc(params,sizeof(char *)*2,char **); \
|
||||
safemalloc(params[0],1*sizeof(char),char *); \
|
||||
params[0][0]='\0'; \
|
||||
safemalloc(params[1],1024*sizeof(char),char *); \
|
||||
} \
|
||||
while(0)
|
||||
|
||||
int WINAPI WinMain(HINSTANCE instance,HINSTANCE previous_instance,LPSTR cmdline,int cmdshow)
|
||||
{
|
||||
if(!cmp(cmdline,"-service"))
|
||||
{
|
||||
SERVICE_TABLE_ENTRY service_table[]={
|
||||
{SERVICE_SHORT_NAME,service_main},
|
||||
{NULL,NULL}
|
||||
private:
|
||||
std::unique_ptr<char*[]> params_;
|
||||
};
|
||||
|
||||
if(0==StartServiceCtrlDispatcher(service_table))
|
||||
{
|
||||
winerror("Error starting service dispatcher.");
|
||||
return -1;
|
||||
// Helper functions
|
||||
void show_error(const char* message) {
|
||||
MessageBox(NULL, message, ("Error from " SERVICE_NAME), MB_OK | MB_ICONERROR);
|
||||
}
|
||||
}
|
||||
else if(!cmp(cmdline,"-install"))
|
||||
service_install();
|
||||
else if(!cmp(cmdline,"-uninstall"))
|
||||
service_uninstall();
|
||||
else
|
||||
{
|
||||
//we know that hermes can only have one parameter, so
|
||||
//just copy it
|
||||
init_params();
|
||||
strncpy(params[1],cmdline,1024);
|
||||
hermes_main(2,(char **)params);
|
||||
free_params();
|
||||
}
|
||||
|
||||
return 0;
|
||||
void show_message(const char* message) {
|
||||
MessageBox(NULL, message, ("Message from " SERVICE_NAME), MB_OK | MB_ICONINFORMATION);
|
||||
}
|
||||
|
||||
void update_service_status(DWORD new_state) {
|
||||
service_status.dwCurrentState = new_state;
|
||||
SetServiceStatus(service_status_handle, &service_status);
|
||||
}
|
||||
}
|
||||
|
||||
static int service_install()
|
||||
{
|
||||
SC_HANDLE scm,service_handle;
|
||||
SERVICE_DESCRIPTION service_description;
|
||||
char filename[1024];
|
||||
string exepath;
|
||||
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous_instance, LPSTR cmdline, int cmdshow) {
|
||||
try {
|
||||
if (strncmp(cmdline, "-service", strlen("-service")) == 0) {
|
||||
SERVICE_TABLE_ENTRY service_table[] = {
|
||||
{const_cast<LPSTR>(SERVICE_SHORT_NAME), service_main},
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
if(NULL==(scm=OpenSCManager(NULL,NULL,SC_MANAGER_CREATE_SERVICE)))
|
||||
{
|
||||
winerror(_("Error opening connection to the Service Manager."));
|
||||
exit(-1);
|
||||
}
|
||||
if(0==GetModuleFileName(NULL,filename,sizeof(filename)))
|
||||
{
|
||||
winerror(_("Error getting the file name of the process."));
|
||||
exit(-1);
|
||||
}
|
||||
if (!StartServiceCtrlDispatcher(service_table)) {
|
||||
throw std::runtime_error("Error starting service dispatcher");
|
||||
}
|
||||
}
|
||||
else if (strncmp(cmdline, "-install", strlen("-install")) == 0) {
|
||||
return service_install();
|
||||
}
|
||||
else if (strncmp(cmdline, "-uninstall", strlen("-uninstall")) == 0) {
|
||||
return service_uninstall();
|
||||
}
|
||||
else {
|
||||
Parameters params;
|
||||
strncpy(params[1], cmdline, 1023);
|
||||
params[1][1023] = '\0'; // Ensure null termination
|
||||
return hermes_main(2, params.get());
|
||||
}
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
show_error(e.what());
|
||||
return -1;
|
||||
}
|
||||
|
||||
exepath=string("\"")+filename+"\" -service";
|
||||
|
||||
service_handle=CreateService(
|
||||
scm, //scm handle
|
||||
SERVICE_SHORT_NAME, //service name
|
||||
SERVICE_NAME, //display name
|
||||
SERVICE_ALL_ACCESS, //desired access
|
||||
SERVICE_WIN32_OWN_PROCESS, //service type
|
||||
SERVICE_AUTO_START, //start type
|
||||
SERVICE_ERROR_NORMAL, //error control
|
||||
exepath.c_str(), //executable path with arguments
|
||||
NULL, //load group
|
||||
NULL, //tag for group id
|
||||
NULL, //dependencies
|
||||
NULL, //user name
|
||||
NULL); //password
|
||||
|
||||
if(NULL==service_handle)
|
||||
{
|
||||
winerror("Error creating service. Already installed?");
|
||||
exit(-1);
|
||||
}
|
||||
else
|
||||
winmessage("Service successfully installed.");
|
||||
|
||||
//createservice doesn't have a field for description
|
||||
//so we use ChangeServiceConfig2
|
||||
service_description.lpDescription=SERVICE_DESCRIPTION_TEXT;
|
||||
ChangeServiceConfig2(service_handle,SERVICE_CONFIG_DESCRIPTION,(void *)&service_description);
|
||||
|
||||
CloseServiceHandle(service_handle);
|
||||
CloseServiceHandle(scm);
|
||||
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int service_uninstall()
|
||||
{
|
||||
SC_HANDLE scm,service_handle;
|
||||
SERVICE_STATUS status;
|
||||
static int service_install() {
|
||||
char filename[1024] = {0};
|
||||
if (GetModuleFileName(NULL, filename, sizeof(filename)) == 0) {
|
||||
throw std::runtime_error("Error getting the file name of the process");
|
||||
}
|
||||
|
||||
if(NULL==(scm=OpenSCManager(NULL,NULL,SC_MANAGER_CREATE_SERVICE)))
|
||||
{
|
||||
winerror(_("Error opening connection to the Service Manager."));
|
||||
exit(-1);
|
||||
}
|
||||
SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
|
||||
if (!scm) {
|
||||
throw std::runtime_error("Error opening connection to the Service Manager");
|
||||
}
|
||||
|
||||
if(NULL==(service_handle=OpenService(scm,SERVICE_SHORT_NAME,SERVICE_QUERY_STATUS|DELETE)))
|
||||
{
|
||||
winerror(_("Error opening service."));
|
||||
CloseServiceHandle(scm);
|
||||
exit(-1);
|
||||
}
|
||||
std::string exepath = "\"" + std::string(filename) + "\" -service";
|
||||
|
||||
SC_HANDLE service_handle = CreateService(
|
||||
scm, // SCM handle
|
||||
SERVICE_SHORT_NAME, // Service name
|
||||
SERVICE_NAME, // Display name
|
||||
SERVICE_ALL_ACCESS, // Desired access
|
||||
SERVICE_WIN32_OWN_PROCESS, // Service type
|
||||
SERVICE_AUTO_START, // Start type
|
||||
SERVICE_ERROR_NORMAL, // Error control
|
||||
exepath.c_str(), // Executable path
|
||||
NULL, NULL, NULL, NULL, NULL // Other parameters
|
||||
);
|
||||
|
||||
if (!service_handle) {
|
||||
CloseServiceHandle(scm);
|
||||
throw std::runtime_error("Error creating service. Already installed?");
|
||||
}
|
||||
|
||||
// Set service description
|
||||
SERVICE_DESCRIPTION service_description = {const_cast<LPSTR>(SERVICE_DESCRIPTION_TEXT)};
|
||||
ChangeServiceConfig2(service_handle, SERVICE_CONFIG_DESCRIPTION, &service_description);
|
||||
|
||||
if(0==QueryServiceStatus(service_handle,&status))
|
||||
{
|
||||
winerror(_("Error querying service."));
|
||||
CloseServiceHandle(scm);
|
||||
CloseServiceHandle(service_handle);
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
if(SERVICE_STOPPED!=status.dwCurrentState)
|
||||
{
|
||||
winerror(SERVICE_NAME _(" is still running. Stop it before trying to uninstall it."));
|
||||
CloseServiceHandle(scm);
|
||||
CloseServiceHandle(service_handle);
|
||||
exit(-1);
|
||||
}
|
||||
show_message("Service successfully installed");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(0==DeleteService(service_handle))
|
||||
{
|
||||
winerror(_("Error deleting service."));
|
||||
static int service_uninstall() {
|
||||
SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
|
||||
if (!scm) {
|
||||
throw std::runtime_error("Error opening connection to the Service Manager");
|
||||
}
|
||||
|
||||
SC_HANDLE service_handle = OpenService(scm, SERVICE_SHORT_NAME, SERVICE_QUERY_STATUS | DELETE);
|
||||
if (!service_handle) {
|
||||
CloseServiceHandle(scm);
|
||||
throw std::runtime_error("Error opening service");
|
||||
}
|
||||
|
||||
SERVICE_STATUS status;
|
||||
if (!QueryServiceStatus(service_handle, &status)) {
|
||||
CloseServiceHandle(service_handle);
|
||||
CloseServiceHandle(scm);
|
||||
throw std::runtime_error("Error querying service");
|
||||
}
|
||||
|
||||
if (status.dwCurrentState != SERVICE_STOPPED) {
|
||||
CloseServiceHandle(service_handle);
|
||||
CloseServiceHandle(scm);
|
||||
throw std::runtime_error(SERVICE_NAME " is still running. Stop it before trying to uninstall it");
|
||||
}
|
||||
|
||||
if (!DeleteService(service_handle)) {
|
||||
CloseServiceHandle(service_handle);
|
||||
CloseServiceHandle(scm);
|
||||
throw std::runtime_error("Error deleting service");
|
||||
}
|
||||
|
||||
CloseServiceHandle(service_handle);
|
||||
CloseServiceHandle(scm);
|
||||
CloseServiceHandle(service_handle);
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
CloseServiceHandle(scm);
|
||||
CloseServiceHandle(service_handle);
|
||||
winmessage(_("Service successfully uninstalled."));
|
||||
return 0;
|
||||
show_message("Service successfully uninstalled");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void WINAPI service_main(DWORD argc,LPTSTR *argv)
|
||||
{
|
||||
char *tmpstr;
|
||||
static void WINAPI service_main(DWORD argc, LPTSTR* argv) {
|
||||
service_status = {
|
||||
.dwServiceType = SERVICE_WIN32,
|
||||
.dwControlsAccepted = SERVICE_ACCEPT_STOP,
|
||||
.dwWin32ExitCode = NO_ERROR,
|
||||
.dwServiceSpecificExitCode = NO_ERROR,
|
||||
.dwCheckPoint = 0,
|
||||
.dwWaitHint = 0
|
||||
};
|
||||
|
||||
//configure service_status structure
|
||||
service_status.dwServiceType=SERVICE_WIN32;
|
||||
service_status.dwControlsAccepted=0;
|
||||
service_status.dwWin32ExitCode=NO_ERROR;
|
||||
service_status.dwServiceSpecificExitCode=NO_ERROR;
|
||||
service_status.dwCheckPoint=0;
|
||||
service_status.dwWaitHint=0;
|
||||
service_status.dwControlsAccepted|=SERVICE_ACCEPT_STOP;
|
||||
service_status_handle = RegisterServiceCtrlHandler(SERVICE_SHORT_NAME, handler);
|
||||
if (!service_status_handle) {
|
||||
return;
|
||||
}
|
||||
|
||||
service_status_handle=RegisterServiceCtrlHandler(SERVICE_SHORT_NAME,handler);
|
||||
update_service_status(SERVICE_RUNNING);
|
||||
|
||||
if(0!=service_status_handle)
|
||||
{
|
||||
//set service status
|
||||
ChangeServiceStatus(service_status_handle,service_status,SERVICE_RUNNING);
|
||||
try {
|
||||
Parameters params;
|
||||
if (GetModuleFileName(NULL, params[1], 1024) == 0) {
|
||||
throw std::runtime_error("Error getting module filename");
|
||||
}
|
||||
|
||||
//get the path to the config file
|
||||
init_params();
|
||||
GetModuleFileName(NULL,params[1],1024);
|
||||
if(NULL==(tmpstr=strrchr(params[1],'\\'))) { winerror("Error finding default config file."); exit(-1); }
|
||||
*(++tmpstr)='\0';
|
||||
strncat(params[1],"hermes.ini",strlen("hermes.ini"));
|
||||
char* config_path = strrchr(params[1], '\\');
|
||||
if (!config_path) {
|
||||
throw std::runtime_error("Error finding default config file");
|
||||
}
|
||||
|
||||
//now start our main program
|
||||
hermes_main(2,(char **)params);
|
||||
*(++config_path) = '\0';
|
||||
strncat(params[1], "hermes.ini", strlen("hermes.ini"));
|
||||
|
||||
free_params();
|
||||
hermes_main(2, params.get());
|
||||
|
||||
//when we are here, we have been stopped
|
||||
ChangeServiceStatus(service_status_handle,service_status,SERVICE_STOP_PENDING);
|
||||
ChangeServiceStatus(service_status_handle,service_status,SERVICE_STOPPED);
|
||||
}
|
||||
update_service_status(SERVICE_STOP_PENDING);
|
||||
update_service_status(SERVICE_STOPPED);
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
show_error(e.what());
|
||||
update_service_status(SERVICE_STOPPED);
|
||||
}
|
||||
}
|
||||
|
||||
static void WINAPI handler(DWORD code)
|
||||
{
|
||||
if(SERVICE_CONTROL_STOP==code)
|
||||
{
|
||||
quit=true;
|
||||
ChangeServiceStatus(service_status_handle,service_status,SERVICE_STOP_PENDING);
|
||||
}
|
||||
static void WINAPI handler(DWORD code) {
|
||||
if (code == SERVICE_CONTROL_STOP) {
|
||||
quit = true;
|
||||
update_service_status(SERVICE_STOP_PENDING);
|
||||
}
|
||||
}
|
||||
|
|
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