From d2564d25e802d1ee3230cf045c4940e836b5c6a2 Mon Sep 17 00:00:00 2001 From: John Denker Date: Sun, 29 Jul 2012 16:50:11 -0700 Subject: split ltgrey (and libltgrey) off from greylist; put some utility functions into their own file. --- .gitignore | 1 + tools/greylist.c | 50 +------- tools/libltgrey.c | 343 ++++++++++++++++++++++++++++++++++++++++++++++++++ tools/libltgrey.h | 38 ++++++ tools/ltgrey.c | 153 ++++++++++++++++++++++ tools/makefile | 12 +- tools/qq_exit_codes.h | 15 +++ tools/skrewt.c | 17 +-- tools/utils.c | 44 +++++++ tools/utils.h | 3 + 10 files changed, 611 insertions(+), 65 deletions(-) create mode 100644 tools/libltgrey.c create mode 100644 tools/libltgrey.h create mode 100644 tools/ltgrey.c create mode 100644 tools/qq_exit_codes.h create mode 100644 tools/utils.c create mode 100644 tools/utils.h diff --git a/.gitignore b/.gitignore index e929027..b6369d1 100644 --- a/.gitignore +++ b/.gitignore @@ -163,3 +163,4 @@ data.tar.gz dummy-mail-transfer-agent_all.deb bash-c wripper +ltgrey diff --git a/tools/greylist.c b/tools/greylist.c index 89396e7..9af70eb 100644 --- a/tools/greylist.c +++ b/tools/greylist.c @@ -22,7 +22,6 @@ #include /* for memset() */ #include /* for inet_ntop() */ - using namespace std; const int minute(60); @@ -33,18 +32,7 @@ const int minimum_age(15*minute); const int maximum_age(32*day); const int probation(4*hour); -// error exit codes, mostly as stated in qmail.c -#define foo(name, num) const int ex_ ## name = num -#define bar foo(good, 0) ;\ -foo(spam, 21) ;\ -foo(penaltybox, 22) ;\ -foo(badDNS, 23) ;\ -foo(greylisting, 70) ;\ -foo(syserr, 71) ;\ -foo(comerr, 74) ; - -bar -#undef foo +#include "qq_exit_codes.h" pid_t mypid; string progname; @@ -58,14 +46,6 @@ void dump(const string var){ else cerr << " is not set." << endl; } - -//////////////// -// little utility to help with argument parsing: -// -int prefix(const string shorter, const string longer){ - return shorter == longer.substr(0, shorter.length()); -} - void exeunt(const int sts){ if (sts == ex_good) exit(sts); @@ -93,6 +73,8 @@ void exeunt(const int sts){ exit(sts); } +#include "utils.h" + class whatsit{ public: string dirname; @@ -126,12 +108,6 @@ public: int check_dns_sub(string &addr, string &host, vector &checked); }; -string basename(const string path){ - size_t where = path.rfind("/"); - if (where != string::npos) return path.substr(1+where); - return path; -} - int whatsit::setup(){ stringstream foo; foo << basename(progname) << suffix @@ -145,26 +121,6 @@ int whatsit::setup(){ return 0; } -string time_out(const int _ttt){ - int ttt(abs(_ttt)); - int sec(ttt % 60); - int min((ttt / 60) % 60); - int hr(ttt / 3600); - stringstream foo; - int didsome(0); - if (_ttt < 0) foo << "-"; - if (hr) { - foo << hr << ":"; - didsome++; - } - if (didsome || min){ - foo << setw(didsome?2:1) << setfill('0') << min << ":"; - didsome++; - } - foo << setw(didsome?2:1) << setfill('0') << sec; - return foo.str(); -} - void scan(const string progid, const string p, const int copies=1){ timeval now; gettimeofday(&now, NULL); diff --git a/tools/libltgrey.c b/tools/libltgrey.c new file mode 100644 index 0000000..d4ec0da --- /dev/null +++ b/tools/libltgrey.c @@ -0,0 +1,343 @@ +#include /* for exit(), getenv() */ +#include +#include +#include + +#include /* for stat(), getaddrinfo() */ +#include /* for stat() */ +#include /* for stat() */ +#include /* for perror */ +#include /* for ENOENT */ +#include /* for ofstream() */ +#include /* for creat() */ +#include /* for gettimeofday() */ +#include /* for stringstream */ +#include /* for kill(), SIGUSR1 */ + +// requires apt-get install libboost-filesystem-dev: +#include + +#include /* for getaddrinfo() */ +#include /* for getaddrinfo() */ +#include /* for memset() */ +#include /* for inet_ntop() */ + +using namespace std; + +const int minute(60); +const int hour(60*minute); +const int day(24*hour); + +const int minimum_age(15*minute); +const int maximum_age(32*day); +const int probation(4*hour); + +#if 0 +void exeunt(const int sts){ + if (sts == ex_good) exit(sts); + +#ifndef PENALIZE_SPAMMERS + if (sts == ex_penaltybox) exit(sts); +#endif + +#ifndef KILL_GROUP + exit(sts); +#endif + + const char* foo = getenv("HI_Q_GROUP"); + if (!foo) exit(sts); + +// No point in signalling ourself: + sighandler_t rslt = signal(SIGUSR1, SIG_IGN); + if (rslt == SIG_ERR) { + cerr << "error setting signal" << endl; + } + int k = kill(-atoi(foo), SIGUSR1); + if (k) { + cerr << "kill failed on group " << atoi(foo) << " ... "; + perror(0); + } + exit(sts); +} +#endif + +#include /* for gettimeofday */ +#include /* for setw */ +#include /* for stat */ +#include /* for stat, creat */ +#include /* for stat, creat */ +#include /* for creat */ +#include /* for ofstream() */ + +#include "libltgrey.h" +#include "utils.h" +#include "qq_exit_codes.h" + +void whatsit::dump(const string var){ + char* str = getenv(var.c_str()); + cerr << progname + << "[" << mypid << "] " + << var; + if (str) cerr << " is set to '" << str << "'" << endl; + else cerr << " is not set." << endl; +} + +int whatsit::setup(){ + stringstream foo; + foo << basename(progname) << suffix + << "[" << mypid << "]"; + progid = foo.str(); + + ipvar = getenv("TCPREMOTEIP"); + if (ipvar) ipbase = ipvar; + hostvar = getenv("TCPREMOTEHOST"); + if (hostvar) hostname = hostvar; + return 0; +} + +void whatsit::update(const string msg, const timeval new_mod, + const timeval new_ac, const int penalty, const int stain){ + if (verbosity){ + if (penalty || stain || verbosity>1) cerr << progid << ": "; + if (penalty) cerr << " penalty " << penalty; + if (stain) cerr << " stain " << stain; + if (verbosity > 1) { + if (penalty || stain) cerr << "+"; // separation, punctuation + cerr << msg << ": " << ipbase; + if (hostname.length()) cerr << " " << hostname; + cerr << " mod_age: " << time_out(mod_age) + << " ac_age: " << time_out(ac_age); + } + cerr << endl; + } + timeval pen_mod(new_mod); + timeval stain_ac(new_ac); + if (penalty) { + pen_mod = now; + pen_mod.tv_sec += penalty; + } + if (stain) { + stain_ac = now; + stain_ac.tv_sec -= stain; + } + timeval upd[2] = { +// beware: access illogically comes *before* modification here: + stain_ac, + pen_mod + }; + if (utimes(ipname.c_str(), upd)) + cerr << "oops" << endl; +} + +int whatsit::doit(const int penalty, const int stain){ + + if (!ipvar) { + cerr << progid + << " TCPREMOTEIP not set???" << endl; + // should never happen + // although you can make it happen using a weird test-harness + return(ex_syserr); + } + +// see if our directory exists: + struct stat dirstat; + int rslt = stat(dirname.c_str(), &dirstat); + if (rslt != 0){ + if (errno != ENOENT) { + cerr << progid << ": stat failed for '" + << dirname << "' : "; + perror(0); + } + rslt = mkdir(dirname.c_str(), 0755); + if (rslt != 0) { + cerr << progid + << "uid " << getuid() + << ": mkdir failed for '" + << dirname << "' : "; + perror(0); + return(ex_syserr); + } + } + + ipname = dirname + "/" + ipbase; + struct stat ipstat; + rslt = stat(ipname.c_str(), &ipstat); + if (rslt != 0){ + if (errno != ENOENT) { + cerr << progid << ": stat failed for '" + << ipname << "' : "; + perror(0); + } + ofstream foo; + int fd = creat(ipname.c_str(), 0644); + if (fd < 0){ + cerr << progid << ": create failed for '" + << ipname << "' : "; + perror(0); + } + close(fd); + update("new customer", now, now, penalty, stain); + return(ex_greylisting); + } + +// now for really checking the greylist status: + mod_age = now.tv_sec - ipstat.st_mtime; + ac_age = now.tv_sec - ipstat.st_atime; + timeval mod_orig = {ipstat.st_mtime, 0}; + if (mod_age < 0) { + update("penalty box", mod_orig, now, penalty, stain); + return(ex_penaltybox); + } + if (mod_age < ac_age){ +// when he comes out on parole, he starts over with no reputation: + update("paroled spammer", now, now, penalty, stain); + return(ex_greylisting); + } + if (mod_age < minimum_age) { + update("early bird", mod_orig, now, penalty, stain); + return(ex_greylisting); + } + if (mod_age - ac_age < minimum_age // early bird, or completely unused + && mod_age > probation) { // did not diligently resubmit + update("disprobation", now, now, penalty, stain); + return(ex_greylisting); + } + if (ac_age > maximum_age) { + update("too old, starting over", now, now, penalty, stain); + return(ex_greylisting); + } +// if all checks are passed, must be OK: + update("returning customer", mod_orig, now, penalty, stain); + return 0; +} + +typedef vector VU; + +class VUx{ +public: + VU addr; + sa_family_t fam; + string str(); +}; + +string VUx::str(){ + char msgbuf[INET6_ADDRSTRLEN]; + const char* rslt = inet_ntop(fam, &addr[0], + msgbuf, sizeof(msgbuf)); + if (!rslt) rslt = ""; + return rslt; +} + +VUx parse_sockaddr(const sockaddr* ai_addr) { + void* numericAddress; + VUx rslt; + int addrsize; + rslt.addr = VU(0); + rslt.fam = ((sockaddr *)ai_addr)->sa_family; + switch (rslt.fam) { + case AF_INET: + numericAddress = &(((sockaddr_in *)ai_addr)->sin_addr.s_addr); + addrsize = sizeof(in_addr); + break; + case AF_INET6: + numericAddress = &(((sockaddr_in6 *)ai_addr)->sin6_addr.s6_addr); + addrsize = sizeof(in6_addr); + break; + default: + cerr << "?Unknown address family " << rslt.fam << endl; + return rslt; + } + unsigned char* foo = (unsigned char*) numericAddress; + rslt.addr = VU(foo, foo+addrsize); + return rslt; +} + +int diff(const VU aaa, const VU bbb){ + if(aaa.size() != bbb.size()) return 1; + for (unsigned int ii=0; ii < aaa.size(); ii++){ + if (aaa[ii] != bbb[ii]) return 1; + } + return 0; +} + +int whatsit::check_dns(){ + string addr("()"), host("()"); + vector checked; + int sts = check_dns_sub(addr, host, checked); + if (sts == 0) return sts; + if (sts != ex_badDNS) return sts; // possible ex_syserr +#if 1 + sts = 0; // demote badDNS to just a warning +#endif + cerr << progid; + if (!sts) cerr << " (warning)"; + cerr << " DNS inconsistency: " + << addr << " --> " + << host << " ==>"; + if (!checked.size()) cerr << " ()"; + else for (vector::const_iterator chk = checked.begin(); + chk != checked.end(); chk++) cerr << " " << *chk; + cerr << endl; + + return sts; +} + +int whatsit::check_dns_sub(string &addr, string &host, vector &checked){ + + struct addrinfo *result; + struct addrinfo *ipresult; + struct addrinfo *res; + addrinfo hints; + int error; + + memset(&hints, 0, sizeof(struct addrinfo)); +#if 1 + // restrict to TCP only; otherwise we get N records per address + hints.ai_protocol = IPPROTO_TCP; +#endif + + error = getaddrinfo(ipvar, NULL, &hints, &ipresult); + if (error == EAI_NONAME) return ex_badDNS; + if (error) { // some unexpected error + cerr << progid + << " odd error " << error + << " in getaddrinfo for " << ipvar + << " : " << gai_strerror(error) << endl; + return ex_syserr; + } + if (!ipresult) { + cerr << "should never happen (addr with no addrs?)" << endl; + return ex_syserr; + } + VUx ipAddr = parse_sockaddr(ipresult->ai_addr); + addr = ipAddr.str(); + + char* hostvar = getenv("TCPREMOTEHOST"); + if (hostvar) host = hostvar; + else return(ex_badDNS); + + error = getaddrinfo(hostvar, NULL, &hints, &result); + if (error == EAI_NONAME) return ex_badDNS; + if (error) { + cerr << progid + << " error " << error + << " compare " << EAI_NONAME + << " in getaddrinfo for " << ipvar + << " :: " << gai_strerror(error) << endl; + return ex_syserr; + } + +// loop over all returned results and check for a match. + for (res = result; res != NULL; res = res->ai_next){ + VUx hostAddr = parse_sockaddr(res->ai_addr); + checked.push_back(hostAddr.str()); + if (!diff(hostAddr.addr, ipAddr.addr)) { + ///// cerr << "match! " << ipAddr.addr.size() << endl; + goto done; + } + } + return ex_badDNS; + +done: + return 0; +} diff --git a/tools/libltgrey.h b/tools/libltgrey.h new file mode 100644 index 0000000..585ec01 --- /dev/null +++ b/tools/libltgrey.h @@ -0,0 +1,38 @@ +#include +#include /* for gettimeofday(), timeval */ +#include + +class whatsit{ +public: + std::string dirname; + std::string progname; + pid_t mypid; + timeval now; + char* ipvar; + char* hostvar; + std::string ipbase; + std::string ipname; + std::string hostname; + int mod_age; + int ac_age; + std::string suffix; + std::string progid; + int verbosity; + + whatsit(const std::string name, const std::string _dirname) + : dirname(_dirname), progname(name), mypid(getpid()), + mod_age(0), ac_age(0), + verbosity(0) + { + gettimeofday(&now, NULL); + } + int doit(const int penalty, const int stain); +// access comes after modification: + void update(const std::string msg, const timeval new_mod, + const timeval new_ac, const int penalty, const int stain); + int setup(); + int check_dns(); + int check_dns_sub(std::string &addr, std::string &host, + std::vector &checked); + void dump(const std::string var); +}; diff --git a/tools/ltgrey.c b/tools/ltgrey.c new file mode 100644 index 0000000..afdb4c1 --- /dev/null +++ b/tools/ltgrey.c @@ -0,0 +1,153 @@ +#include +#include /* for exit(), atoi() */ + +#include "libltgrey.h" +#include "utils.h" +#include "qq_exit_codes.h" + +using namespace std; +pid_t mypid; +string progname; + +#define exeunt exit + +// forward reference: +void scan(const string progid, const string p, const int copies=1); + +int main(int _argc, char** _argv){ + mypid = getpid(); + int argc(_argc); + char** argv(_argv); + const string dirname("/var/qmail/greylist"); + whatsit foo(argv[0], dirname); argc--; argv++; + int scanmode(0); + int copies(1); + int penalty(0); + int stain(0); + int check(0); + while (argc > 0) { + string arg = argv[0]; argc--; argv++; + if (prefix(arg, "-scan")) { + scanmode++; + } else if (prefix(arg, "-copy")) { + copies++; + } else if (prefix(arg, "-verbose")) { + foo.verbosity++; + } else if (prefix(arg, "-check")) { + check++; + } else if (prefix(arg, "-penalize") + || prefix(arg, "-penalty")) { + if (!argc){ + cerr << "Option '" << arg << "' requires an argument" << endl; + exeunt(ex_syserr); + } + penalty = atoi(*argv++); argc--; + } else if (prefix(arg, "-stain")) { + if (!argc){ + cerr << "Option '" << arg << "' requires an argument" << endl; + exeunt(ex_syserr); + } + stain = atoi(*argv++); argc--; + } else if (prefix(arg, "-suffix")) { + if (!argc){ + cerr << "Option '" << arg << "' requires an argument" << endl; + exeunt(ex_syserr); + } + foo.suffix += *argv++; argc--; + } else { + cerr << "Unrecognized arg: " << arg << endl; + exeunt(ex_syserr); + } + } + if (foo.setup()) return ex_syserr; + + if (scanmode) { + scan(foo.progid, dirname, copies); + return 0; + } + + int sts = foo.doit(penalty, stain); + if (sts == ex_syserr) return sts; + if (!check) return ex_good; + +// check mode ... perform some extra checks. +// Probably a better design would be to +// (a) make more thorough DNS checks, and +// (b) move all the DNS checking to a separate module + + int dns = foo.check_dns(); + if (dns == ex_syserr || dns == ex_spam) return dns; + exeunt(sts); +} + +////////////////////////////////////////////////////////////////////// +// requires apt-get install libboost-filesystem-dev: +#include +#include +#include /* for stat(), getaddrinfo() */ +#include /* for stat() */ +#include /* for stat() */ +#include /* for perror */ +#include + +const int minute(60); +const int hour(60*minute); +const int day(24*hour); + +const int minimum_age(15*minute); +const int maximum_age(32*day); +const int probation(4*hour); + +void scan(const string progid, const string p, const int copies){ + timeval now; + gettimeofday(&now, NULL); + using namespace boost::filesystem; + + if (is_directory(p)) { + for (directory_iterator itr(p); itr!=directory_iterator(); ++itr) { + string basename = itr->path().filename(); + for (int ii = 0; ii < copies; ii++) + cout << setw(20) << left << basename << ' '; // display filename only + if (is_regular_file(itr->status())) { +// cout << " [" << file_size(itr->path()) << ']'; + struct stat mystat; + string fn = p + "/" + basename; + int rslt = stat(fn.c_str(), &mystat); + if (rslt != 0){ + cerr << progid << ": stat failed for '" + << fn << "' : "; + perror(0); + } + int mod_age = now.tv_sec - mystat.st_mtime; + int ac_age = now.tv_sec - mystat.st_atime; + cout << setw(10) << time_out(mod_age) + << " " << setw(10) << time_out(ac_age); + if (0) { + + } else if (mod_age < 0) { + cout << " penalty"; + } else if (mod_age < ac_age) { + cout << " parole"; + } else if (mod_age - ac_age < minimum_age // early bird, or completely unused + && mod_age > probation) { // did not diligently resubmit + cout << " disprobation"; + if (mod_age != ac_age) cout << "!"; + } else if (mod_age < minimum_age) { + cout << " young"; + if (mod_age != ac_age) cout << "!"; + } else if (mod_age == ac_age) { + cout << " unused"; + } else if (mod_age > maximum_age) { + cout << " expired"; + } else { + cout << " OK"; + } + } + cout << '\n'; + } + } + else { + // starting point is not a directory: + cout << (exists(p) ? "Found: " : "Not found: ") << p << '\n'; + } +} diff --git a/tools/makefile b/tools/makefile index 76df23b..f0a3f70 100644 --- a/tools/makefile +++ b/tools/makefile @@ -15,10 +15,10 @@ qmain = pido.c hi-q.c skrewt.c hi-test.c mail-scan.c greylist.c wripper.c qprogs = $(qmain:%.c=%) # sources for other main programs: -moremain = wripper.c bash-c.c +moremain = wripper.c bash-c.c ltgrey.c moreprogs = $(moremain:%.c=%) -nonmain = +nonmain = libltgrey.c sources = $(qmain) $(moremain) $(nonmain) @@ -37,10 +37,14 @@ all: $(qprogs) $(moreprogs) show: : --- $(qprogs) +++ $(moreprogs) -greylist: greylist.o +skrewt: skrewt.o $(CC) $< -lboost_filesystem-mt -lboost_system -o $@ -# $(CC) $< -lboost_filesystem -o $@ +greylist: greylist.o utils.o + $(CC) $^ -lboost_filesystem-mt -lboost_system -o $@ + +ltgrey: ltgrey.o utils.o libltgrey.o + $(CC) $^ -lboost_filesystem-mt -lboost_system -o $@ wripper: wripper.o $(CC) $< -o $@ diff --git a/tools/qq_exit_codes.h b/tools/qq_exit_codes.h new file mode 100644 index 0000000..2af6848 --- /dev/null +++ b/tools/qq_exit_codes.h @@ -0,0 +1,15 @@ +// error exit codes, mostly as stated in qmail.c +#define qq_exit_codes \ +foo(good, 0) ;\ +foo(spam, 21) ;\ +foo(penaltybox, 22) ;\ +foo(badDNS, 23) ;\ +foo(usage, 39) ;\ +foo(greylisting, 70) ;\ +foo(syserr, 71) ;\ +foo(comerr, 74) ; + +// expand the codes to make some names: +#define foo(name, num) const int ex_ ## name = num +qq_exit_codes +#undef foo diff --git a/tools/skrewt.c b/tools/skrewt.c index a43fd13..3fee644 100644 --- a/tools/skrewt.c +++ b/tools/skrewt.c @@ -37,19 +37,7 @@ void usage(const int sts){ exit(sts); } -// error exit codes, mostly as stated in qmail.c -#define ErrorCodes \ -foo(good, 0) ;\ -foo(spam, 21) ;\ -foo(permerr, 31) ;\ -foo(usage, 39) ;\ -foo(greylisting, 70) ;\ -foo(syserr, 71) ;\ -foo(comerr, 74) ; - -#define foo(name, num) const int ex_ ## name = num -ErrorCodes -#undef foo +#include "qq_exit_codes.h" ///////////////////////////////////////////////////////// @@ -316,8 +304,8 @@ int main(int _argc, const char** _argv){ headrec += "\n" + noCR(line); } // here with a fully assembled header record +// headrec (unlike line) contains no DOS CR characters int len = headrec.length(); - if (len && headrec[len-1] == '\r') len--; // reduced length, not counting if (len == 0) { saw_blank_line = 1; break; // no more headers in this message @@ -351,6 +339,7 @@ int main(int _argc, const char** _argv){ if (0) if (recno <= 6) cerr << progid << "#" << recno << " " << headrec << endl; } + if (saw_blank_line) {/* ignore */} cerr << progid <<" Mid '" << message_id << "'" << endl; // Headers are done. diff --git a/tools/utils.c b/tools/utils.c new file mode 100644 index 0000000..3ec6e4c --- /dev/null +++ b/tools/utils.c @@ -0,0 +1,44 @@ +#include +#include +#include +//#include /* for abs() */ +#include + +// strip off the directory part of a path, leaving just +// the basic filename +std::string basename(const std::string path){ + size_t where = path.rfind("/"); + if (where != std::string::npos) return path.substr(1+where); + return path; +} + +//////////////// +// little utility to help with argument parsing: +// +int prefix(const std::string shorter, const std::string longer){ + return shorter == longer.substr(0, shorter.length()); +} + +/////////////// +// print a time as (-)hh:mm:ss +// +std::string time_out(const int _ttt){ +using namespace std; + int ttt(abs(_ttt)); + int sec(ttt % 60); + int min((ttt / 60) % 60); + int hr(ttt / 3600); + stringstream foo; + int didsome(0); + if (_ttt < 0) foo << "-"; + if (hr) { + foo << hr << ":"; + didsome++; + } + if (didsome || min){ + foo << setw(didsome?2:1) << setfill('0') << min << ":"; + didsome++; + } + foo << setw(didsome?2:1) << setfill('0') << sec; + return foo.str(); +} diff --git a/tools/utils.h b/tools/utils.h new file mode 100644 index 0000000..450db85 --- /dev/null +++ b/tools/utils.h @@ -0,0 +1,3 @@ +std::string basename(const std::string path); +int prefix(const std::string shorter, const std::string longer); +std::string time_out(const int _ttt); -- cgit v1.2.3