This commit is contained in:
parent
b05c4ad5d9
commit
3c8bd791e6
2 changed files with 163 additions and 137 deletions
|
@ -1,7 +1,8 @@
|
|||
// Proxy.h
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include "SocketInterface.h"
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
|
||||
#define SMTP_STATE_WAIT_FOR_HELO 0
|
||||
#define SMTP_STATE_WAIT_FOR_MAILFROM 1
|
||||
|
@ -10,10 +11,10 @@
|
|||
|
||||
class Proxy {
|
||||
public:
|
||||
Proxy(SocketInterface* outside_socket) : outside(outside_socket) {}
|
||||
|
||||
void run(std::string& peer_address);
|
||||
Proxy();
|
||||
void run(boost::asio::ssl::stream<boost::asio::ip::tcp::socket>* outside);
|
||||
|
||||
private:
|
||||
SocketInterface* outside;
|
||||
boost::asio::io_service& io_service_;
|
||||
boost::asio::ssl::context ssl_context;
|
||||
};
|
||||
|
|
289
src/Proxy.cpp
289
src/Proxy.cpp
|
@ -1,13 +1,17 @@
|
|||
// Proxy.cpp
|
||||
#include "Proxy.h"
|
||||
#include "HostnameResolver.h"
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include "Utils.h" // Dummy include; replace with actual Utils implementation
|
||||
#include "Configfile.h" // Dummy include; replace with actual Configfile implementation
|
||||
#include <format>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include "Utils.h"
|
||||
#include "Configfile.h"
|
||||
|
||||
extern Configfile cfg; // External configuration
|
||||
extern Configfile cfg;
|
||||
|
||||
void Proxy::run(std::string& peer_address) {
|
||||
void Proxy::run(boost::asio::ssl::stream<boost::asio::ip::tcp::socket>* outside) {
|
||||
boost::asio::ssl::stream<boost::asio::ip::tcp::socket>* inside;
|
||||
// Original comments and variables retained
|
||||
std::string from = "";
|
||||
std::string to = "";
|
||||
|
@ -16,7 +20,24 @@ void Proxy::run(std::string& peer_address) {
|
|||
unsigned char last_state = SMTP_STATE_WAIT_FOR_HELO;
|
||||
long unimplemented_requests = 0;
|
||||
|
||||
#ifdef HAVE_SPF
|
||||
SpfChecker spf_checker;
|
||||
#endif
|
||||
|
||||
try {
|
||||
// Resolve hostname using the Boost resolver
|
||||
try {
|
||||
resolvedname = HostnameResolver::resolveHostname(peer_address);
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << std::format("Hostname resolution error: {}", e.what()) << std::endl;
|
||||
resolvedname = "";
|
||||
}
|
||||
|
||||
// Configure SSL contexts
|
||||
boost::asio::ssl::context ssl_context(boost::asio::ssl::context::tlsv12);
|
||||
ssl_context.set_verify_mode(boost::asio::ssl::verify_none);
|
||||
|
||||
bool throttled = cfg.getThrottle(); // Start with a throttled connection
|
||||
bool authenticated = false; // Start with a non-authenticated connection
|
||||
bool esmtp = false;
|
||||
|
@ -24,7 +45,7 @@ void Proxy::run(std::string& peer_address) {
|
|||
std::string hermes_status = "unknown";
|
||||
|
||||
// Check whitelist
|
||||
if (!cfg.getDnsWhitelistDomains().empty() &&
|
||||
if (!cfg.getDnsWhitelistDomains().empty() &&
|
||||
Utils::listed_on_dns_lists(cfg.getDnsWhitelistDomains(), cfg.getDnsWhitelistPercentage(), peer_address)) {
|
||||
authenticated = true;
|
||||
hermes_status = "whitelisted";
|
||||
|
@ -39,195 +60,199 @@ void Proxy::run(std::string& peer_address) {
|
|||
} else {
|
||||
if (!cfg.getAllowDataBeforeBanner()) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(cfg.getBannerDelayTime()));
|
||||
if (outside->canRead(0)) { // if we have data waiting before the server gives us a 220
|
||||
std::cout << "421 (data_before_banner) (ip:" << peer_address << ")\n"; // Log it
|
||||
std::this_thread::sleep_for(std::chrono::seconds(20)); // Annoy spammers once more
|
||||
outside->writeLine("421 Stop sending data before we show you the banner");
|
||||
|
||||
// Check if data is waiting before server banner
|
||||
boost::system::error_code ec;
|
||||
size_t available = outside->lowest_layer().available(ec);
|
||||
if (ec || available > 0) {
|
||||
std::cout << std::format("421 (data_before_banner) (ip:{})\n", peer_address);
|
||||
std::this_thread::sleep_for(std::chrono::seconds(20));
|
||||
|
||||
// Write rejection message
|
||||
std::string rejection_msg = "421 Stop sending data before we show you the banner\r\n";
|
||||
boost::asio::write(outside->lowest_layer(), boost::asio::buffer(rejection_msg), ec);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to the inside server
|
||||
inside.connect(cfg.getServerHost(), cfg.getServerPort());
|
||||
boost::asio::ip::tcp::resolver resolver(io_service_);
|
||||
boost::asio::ip::tcp::resolver::results_type endpoints =
|
||||
resolver.resolve(cfg.getServerHost(), std::to_string(cfg.getServerPort()));
|
||||
|
||||
#ifdef HAVE_SSL
|
||||
boost::asio::connect(inside.lowest_layer(), endpoints);
|
||||
|
||||
// SSL setup
|
||||
if (cfg.getOutgoingSsl()) {
|
||||
inside.prepareSSL(false);
|
||||
inside.startSSL(false);
|
||||
inside.set_verify_mode(boost::asio::ssl::verify_none);
|
||||
inside.handshake(boost::asio::ssl::stream_base::client);
|
||||
}
|
||||
|
||||
if (cfg.getIncomingSsl()) {
|
||||
outside->prepareSSL(true);
|
||||
outside->startSSL(true);
|
||||
outside->set_verify_mode(boost::asio::ssl::verify_none);
|
||||
outside->handshake(boost::asio::ssl::stream_base::server);
|
||||
}
|
||||
#endif // HAVE_SSL
|
||||
|
||||
// Communication buffers
|
||||
std::vector<char> read_buffer(4096);
|
||||
boost::system::error_code ec;
|
||||
|
||||
// Main loop for communication
|
||||
while (!outside->isClosed() && !inside.isClosed()) {
|
||||
while (!outside->lowest_layer().is_open() || !inside.lowest_layer().is_open()) {
|
||||
// Check if the client wants to send something to the server
|
||||
if (outside->canRead(0.2)) {
|
||||
strtemp = outside->readLine();
|
||||
if (outside->isClosed()) return;
|
||||
size_t client_available = outside->lowest_layer().available(ec);
|
||||
if (client_available > 0) {
|
||||
size_t bytes_read = outside->read_some(boost::asio::buffer(read_buffer), ec);
|
||||
strtemp = std::string(read_buffer.begin(), read_buffer.begin() + bytes_read);
|
||||
|
||||
if (strtemp.length() > 10 && "mail from:" == Utils::strtolower(strtemp.substr(0, 10))) {
|
||||
from = Utils::getmail(strtemp);
|
||||
last_state = SMTP_STATE_WAIT_FOR_RCPTTO;
|
||||
}
|
||||
|
||||
if ("ehlo" == Utils::strtolower(strtemp.substr(0, 4))) esmtp = true;
|
||||
if (strtemp.length() > 4 && ("ehlo" == Utils::strtolower(strtemp.substr(0, 4)) ||
|
||||
|
||||
if (strtemp.length() > 4 && ("ehlo" == Utils::strtolower(strtemp.substr(0, 4)) ||
|
||||
"helo" == Utils::strtolower(strtemp.substr(0, 4)))) {
|
||||
ehlostr = Utils::trim(strtemp.substr(5));
|
||||
last_state = SMTP_STATE_WAIT_FOR_MAILFROM;
|
||||
}
|
||||
|
||||
// RCPT TO handling with comprehensive checks
|
||||
if (strtemp.length() > 8 && "rcpt to:" == Utils::strtolower(strtemp.substr(0, 8))) {
|
||||
std::string strlog = "";
|
||||
std::string code = "";
|
||||
std::string mechanism = "";
|
||||
std::string message = "";
|
||||
to = Utils::getmail(strtemp);
|
||||
|
||||
try {
|
||||
resolvedname = Socket::resolveInverselyToString(peer_address);
|
||||
} catch (Exception& e) {
|
||||
resolvedname = "";
|
||||
}
|
||||
// Construct log string using std::format
|
||||
std::string strlog = std::format(
|
||||
"from {} (ip:{}, hostname:{}, {} {}:{}) -> to {}",
|
||||
from,
|
||||
peer_address,
|
||||
resolvedname,
|
||||
(esmtp ? "ehlo" : "helo"),
|
||||
ehlostr,
|
||||
to
|
||||
);
|
||||
|
||||
strlog = "from " + from + " (ip:" + peer_address + ", hostname:" + resolvedname +
|
||||
", " + (esmtp ? "ehlo" : "helo") + ":" + ehlostr + ") -> to " + to;
|
||||
|
||||
// Check greylisting
|
||||
if (cfg.getGreylist() && !authenticated && Utils::greylist(cfg.getDatabaseFile(), peer_address, from, to)) {
|
||||
// Greylisting check
|
||||
std::string code = "250";
|
||||
if (cfg.getGreylist() && !authenticated &&
|
||||
Utils::greylist(cfg.getDatabaseFile(), peer_address, from, to)) {
|
||||
code = "421";
|
||||
mechanism = "greylist";
|
||||
message = code + " Greylisted!! Please try again in a few minutes.";
|
||||
std::cout << "checking " << mechanism << "\n";
|
||||
message = std::format("{} Greylisted!! Please try again in a few minutes.", code);
|
||||
std::cout << std::format("checking {}\n", mechanism);
|
||||
}
|
||||
// SPF Check
|
||||
#ifdef HAVE_SPF
|
||||
else if (cfg.getQuerySpf() && !authenticated && !spf_checker.query(peer_address, ehlostr, from)) {
|
||||
hermes_status = "spf-failed";
|
||||
if (cfg.getAddStatusHeader()) code = "250";
|
||||
else code = cfg.getReturnTempErrorOnReject() ? "421" : "550";
|
||||
else if (cfg.getQuerySpf() && !authenticated &&
|
||||
!spf_checker.query(peer_address, ehlostr, from)) {
|
||||
code = cfg.getAddStatusHeader() ? "250" :
|
||||
(cfg.getReturnTempErrorOnReject() ? "421" : "550");
|
||||
mechanism = "spf";
|
||||
message = code + " You do not seem to be allowed to send email for that particular domain.";
|
||||
std::cout << "checking " << mechanism << "\n";
|
||||
message = std::format(
|
||||
"{} You do not seem to be allowed to send email for that particular domain.",
|
||||
code
|
||||
);
|
||||
std::cout << std::format("checking {}\n", mechanism);
|
||||
}
|
||||
#endif // HAVE_SPF
|
||||
// Check blacklist
|
||||
else if (!authenticated && Utils::blacklisted(cfg.getDatabaseFile(), peer_address, to)) {
|
||||
#endif
|
||||
// Blacklist check
|
||||
else if (!authenticated &&
|
||||
Utils::blacklisted(cfg.getDatabaseFile(), peer_address, to)) {
|
||||
code = cfg.getReturnTempErrorOnReject() ? "421" : "550";
|
||||
mechanism = "allowed-domain-per-ip";
|
||||
message = code + " You do not seem to be allowed to send email to that particular domain from that address.";
|
||||
std::cout << "checking " << mechanism << "\n";
|
||||
message = std::format(
|
||||
"{} You do not seem to be allowed to send email to that particular domain from that address.",
|
||||
code
|
||||
);
|
||||
std::cout << std::format("checking {}\n", mechanism);
|
||||
}
|
||||
// Check RBL
|
||||
else if (!cfg.getDnsBlacklistDomains().empty() && !authenticated &&
|
||||
Utils::listed_on_dns_lists(cfg.getDnsBlacklistDomains(), cfg.getDnsBlacklistPercentage(), peer_address)) {
|
||||
hermes_status = "blacklisted";
|
||||
if (cfg.getAddStatusHeader()) code = "250";
|
||||
else code = cfg.getReturnTempErrorOnReject() ? "421" : "550";
|
||||
// DNS Blacklist check
|
||||
else if (!cfg.getDnsBlacklistDomains().empty() && !authenticated &&
|
||||
Utils::listed_on_dns_lists(cfg.getDnsBlacklistDomains(),
|
||||
cfg.getDnsBlacklistPercentage(),
|
||||
peer_address)) {
|
||||
code = cfg.getAddStatusHeader() ? "250" :
|
||||
(cfg.getReturnTempErrorOnReject() ? "421" : "550");
|
||||
mechanism = "dnsbl";
|
||||
message = code + " You are listed on some DNS blacklists. Get delisted before trying to send us email.";
|
||||
std::cout << "checking " << mechanism << "\n";
|
||||
message = std::format(
|
||||
"{} You are listed on some DNS blacklists. Get delisted before trying to send us email.",
|
||||
code
|
||||
);
|
||||
std::cout << std::format("checking {}\n", mechanism);
|
||||
}
|
||||
else if (cfg.getRejectNoReverseResolution() && !authenticated && "" == resolvedname) {
|
||||
// Reverse DNS check
|
||||
else if (cfg.getRejectNoReverseResolution() && !authenticated && resolvedname.empty()) {
|
||||
code = cfg.getReturnTempErrorOnReject() ? "421" : "550";
|
||||
mechanism = "no reverse resolution";
|
||||
message = code + " Your IP address does not resolve to a hostname.";
|
||||
std::cout << "checking " << mechanism << "\n";
|
||||
message = std::format(
|
||||
"{} Your IP address does not resolve to a hostname.",
|
||||
code
|
||||
);
|
||||
std::cout << std::format("checking {}\n", mechanism);
|
||||
}
|
||||
// HELO/Reverse name check
|
||||
else if (cfg.getCheckHeloAgainstReverse() && !authenticated && ehlostr != resolvedname) {
|
||||
code = cfg.getReturnTempErrorOnReject() ? "421" : "550";
|
||||
mechanism = "helo differs from resolved name";
|
||||
message = code + " Your IP hostname doesn't match your envelope hostname.";
|
||||
std::cout << "checking " << mechanism << "\n";
|
||||
} else {
|
||||
code = "250";
|
||||
message = std::format(
|
||||
"{} Your IP hostname doesn't match your envelope hostname.",
|
||||
code
|
||||
);
|
||||
std::cout << std::format("checking {}\n", mechanism);
|
||||
}
|
||||
|
||||
if (!mechanism.empty()) strlog.insert(0, "(" + mechanism + ") ");
|
||||
strlog.insert(0, code + " ");
|
||||
std::cout << strlog << "\n"; // Log the connection
|
||||
// Prepare log message
|
||||
if (!mechanism.empty()) {
|
||||
strlog = std::format("({}) {}", mechanism, strlog);
|
||||
}
|
||||
strlog = std::format("{} {}", code, strlog);
|
||||
std::cout << strlog << "\n";
|
||||
|
||||
// If we didn't accept the email, punish spammers
|
||||
if ("250" != code) {
|
||||
inside.writeLine("QUIT");
|
||||
inside.close(); // Close the socket now and leave server alone
|
||||
// Handle rejection
|
||||
if (code != "250") {
|
||||
// Close inside connection
|
||||
inside.lowest_layer().close();
|
||||
|
||||
// Delay to annoy spammers
|
||||
std::this_thread::sleep_for(std::chrono::seconds(20));
|
||||
outside->writeLine(message);
|
||||
|
||||
// Send rejection message
|
||||
std::string rejection_msg = message + "\r\n";
|
||||
boost::asio::write(outside->lowest_layer(), boost::asio::buffer(rejection_msg), ec);
|
||||
return;
|
||||
}
|
||||
|
||||
last_state = SMTP_STATE_WAIT_FOR_DATA;
|
||||
}
|
||||
|
||||
// Handle STARTTLS
|
||||
if ("starttls" == Utils::strtolower(strtemp.substr(0, 8))) {
|
||||
#ifdef HAVE_SSL
|
||||
try {
|
||||
outside->prepareSSL(true);
|
||||
std::cout << "STARTTLS issued by remote, TLS enabled\n";
|
||||
outside->writeLine("220 You can speak now, line is secure!!");
|
||||
outside->startSSL(true);
|
||||
} catch (Exception& e) {
|
||||
std::cout << "STARTTLS issued by remote, but enableSSL failed!\n";
|
||||
outside->writeLine("454 Tried to enable SSL but failed");
|
||||
}
|
||||
#else
|
||||
outside->writeLine("454 TLS temporarily not available");
|
||||
std::cout << "STARTTLS issued by remote, TLS was not enabled because this build lacks SSL support\n";
|
||||
#endif // HAVE_SSL
|
||||
strtemp = "";
|
||||
}
|
||||
|
||||
if (strtemp.length()) inside.writeLine(strtemp);
|
||||
// Send to inside server
|
||||
boost::asio::write(inside.lowest_layer(), boost::asio::buffer(strtemp), ec);
|
||||
}
|
||||
|
||||
// Check if the server wants to send something to the client
|
||||
if (inside.canRead(0.2)) {
|
||||
strtemp = inside.readLine();
|
||||
if (inside.isClosed()) return;
|
||||
size_t server_available = inside.lowest_layer().available(ec);
|
||||
if (server_available > 0) {
|
||||
size_t bytes_read = inside.read_some(boost::asio::buffer(read_buffer), ec);
|
||||
strtemp = std::string(read_buffer.begin(), read_buffer.begin() + bytes_read);
|
||||
|
||||
std::string code = strtemp.substr(0, 3); // All responses by the server start with a code
|
||||
if ("354" == code) { // 354 -> you can start sending data, unthrottle now
|
||||
std::string endofdata = "";
|
||||
ssize_t bytes_read = 0;
|
||||
char buffer[4097];
|
||||
outside->writeLine(strtemp);
|
||||
|
||||
do {
|
||||
bytes_read = outside->readBytes(buffer, sizeof(buffer) - 1);
|
||||
if (bytes_read < 1) throw NetworkException("Problem reading DATA contents, recv returned " + Utils::inttostr(bytes_read), __FILE__, __LINE__);
|
||||
buffer[bytes_read] = '\0';
|
||||
inside.writeBytes(buffer, bytes_read);
|
||||
if (bytes_read < 5) endofdata += std::string(buffer);
|
||||
else endofdata = std::string(buffer + bytes_read - 5);
|
||||
if (endofdata.length() > 5) endofdata = endofdata.substr(endofdata.length() - 5);
|
||||
} while (endofdata != "\r\n.\r\n" && endofdata.length() > 3 && endofdata.substr(2) != "\n.\n" && endofdata.substr(2) != "\r.\r");
|
||||
}
|
||||
|
||||
if ("235" == code) { // 235 -> you are correctly authenticated
|
||||
throttled = false;
|
||||
authenticated = true;
|
||||
hermes_status = "authenticated";
|
||||
}
|
||||
|
||||
// Try to annoy spammers who send too many senseless commands by delaying their connection
|
||||
if ("502" == code) { // 502 unimplemented
|
||||
if (cfg.getNumberOfUnimplementedCommandsAllowed() != -1 && ++unimplemented_requests > cfg.getNumberOfUnimplementedCommandsAllowed()) {
|
||||
inside.writeLine("QUIT");
|
||||
inside.close(); // Close the socket now and leave server alone
|
||||
std::this_thread::sleep_for(std::chrono::seconds(60));
|
||||
outside->writeLine("502 Too many unimplemented commands, closing connection");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (strtemp.length()) outside->writeLine(strtemp);
|
||||
// Send to outside socket
|
||||
boost::asio::write(outside->lowest_layer(), boost::asio::buffer(strtemp), ec);
|
||||
}
|
||||
|
||||
if (throttled) std::this_thread::sleep_for(std::chrono::seconds(cfg.getThrottlingTime())); // Take 1 second between each command
|
||||
// Throttling
|
||||
if (throttled) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(cfg.getThrottlingTime()));
|
||||
}
|
||||
}
|
||||
} catch (Exception& e) { // Any exception will close both connections
|
||||
std::cerr << "Exception occurred: " << e.what() << std::endl;
|
||||
return;
|
||||
}
|
||||
catch (const boost::system::system_error& e) {
|
||||
std::cerr << std::format("Boost.Asio error: {}", e.what()) << std::endl;
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << std::format("Standard exception: {}", e.what()) << std::endl;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue