From: Tom Kistner Date: Fri, 26 Nov 2004 09:13:34 +0000 (+0000) Subject: Extra exiscan source files - first batch X-Git-Url: https://git.exim.org/users/jgh/exim.git/commitdiff_plain/a185d2b536238ebb10877465825ee8bff69ad790 Extra exiscan source files - first batch --- diff --git a/src/src/malware.c b/src/src/malware.c new file mode 100644 index 000000000..ec5b1fbe0 --- /dev/null +++ b/src/src/malware.c @@ -0,0 +1,1417 @@ +/* $Cambridge: exim/src/src/malware.c,v 1.1.2.1 2004/11/26 09:13:34 tom Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* This file is part of the exiscan-acl content scanner +patch. It is NOT part of the standard exim distribution. */ + +/* Copyright (c) Tom Kistner 2003-???? */ +/* License: GPL */ + +/* Code for calling virus (malware) scanners. Called from acl.c. */ + +#include "exim.h" + +/* declaration of private routines */ +int mksd_scan_packed(int sock); +int mksd_scan_unpacked(int sock, int maxproc); + +/* SHUT_WR seems to be undefined on Unixware ? */ +#ifndef SHUT_WR +#define SHUT_WR 1 +#endif + +#define DRWEBD_SCAN_CMD (1) /* scan file, buffer or diskfile */ +#define DRWEBD_RETURN_VIRUSES (1<<0) /* ask daemon return to us viruses names from report */ +#define DRWEBD_IS_MAIL (1<<19) /* say to daemon that format is "archive MAIL" */ + +/* Routine to check whether a system is big- or litte-endian. + Ripped from http://www.faqs.org/faqs/graphics/fileformats-faq/part4/section-7.html + Needed for proper kavdaemon implementation. Sigh. */ +#define BIG_MY_ENDIAN 0 +#define LITTLE_MY_ENDIAN 1 +int test_byte_order(void); +int test_byte_order() { + short int word = 0x0001; + char *byte = (char *) &word; + return(byte[0] ? LITTLE_MY_ENDIAN : BIG_MY_ENDIAN); +} + +uschar malware_name_buffer[256]; +int malware_ok = 0; + +int malware(uschar **listptr) { + int sep = 0; + uschar *list = *listptr; + uschar *av_scanner_work = av_scanner; + uschar *scanner_name; + uschar scanner_name_buffer[16]; + uschar *malware_regex; + uschar malware_regex_buffer[64]; + uschar malware_regex_default[] = ".+"; + unsigned long long mbox_size; + FILE *mbox_file; + int roffset; + const pcre *re; + const uschar *rerror; + + /* make sure the eml mbox file is spooled up */ + mbox_file = spool_mbox(&mbox_size); + if (mbox_file == NULL) { + /* error while spooling */ + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: error while creating mbox spool file"); + return DEFER; + }; + /* none of our current scanners need the mbox + file as a stream, so we can close it right away */ + fclose(mbox_file); + + /* extract the malware regex to match against from the option list */ + if ((malware_regex = string_nextinlist(&list, &sep, + malware_regex_buffer, + sizeof(malware_regex_buffer))) != NULL) { + + /* parse 1st option */ + if ( (strcmpic(malware_regex,US"false") == 0) || + (Ustrcmp(malware_regex,"0") == 0) ) { + /* explicitly no matching */ + return FAIL; + }; + + /* special cases (match anything except empty) */ + if ( (strcmpic(malware_regex,US"true") == 0) || + (Ustrcmp(malware_regex,"*") == 0) || + (Ustrcmp(malware_regex,"1") == 0) ) { + malware_regex = malware_regex_default; + }; + } + else { + /* empty means "don't match anything" */ + return FAIL; + }; + + /* compile the regex, see if it works */ + re = pcre_compile(CS malware_regex, PCRE_COPT, (const char **)&rerror, &roffset, NULL); + if (re == NULL) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: regular expression error in '%s': %s at offset %d", malware_regex, rerror, roffset); + return DEFER; + }; + + /* if av_scanner starts with a dollar, expand it first */ + if (*av_scanner == '$') { + av_scanner_work = expand_string(av_scanner); + if (av_scanner_work == NULL) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: av_scanner starts with $, but expansion failed: %s", expand_string_message); + return DEFER; + } + else { + debug_printf("Expanded av_scanner global: %s\n", av_scanner_work); + /* disable result caching in this case */ + malware_name = NULL; + malware_ok = 0; + }; + } + + /* Do not scan twice. */ + if (malware_ok == 0) { + + /* find the scanner type from the av_scanner option */ + if ((scanner_name = string_nextinlist(&av_scanner_work, &sep, + scanner_name_buffer, + sizeof(scanner_name_buffer))) == NULL) { + /* no scanner given */ + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: av_scanner configuration variable is empty"); + return DEFER; + }; + + /* "drweb" scanner type ----------------------------------------------- */ + /* v0.1 - added support for tcp sockets */ + /* v0.0 - initial release -- support for unix sockets */ + if (strcmpic(scanner_name,US"drweb") == 0) { + uschar *drweb_options; + uschar drweb_options_buffer[1024]; + uschar drweb_options_default[] = "/usr/local/drweb/run/drwebd.sock"; + struct sockaddr_un server; + int sock, port, result, ovector[30]; + unsigned int fsize; + uschar tmpbuf[1024], *drweb_fbuf; + uschar scanrequest[1024]; + uschar drweb_match_string[128]; + int drweb_rc, drweb_cmd, drweb_flags = 0x0000, drweb_fd, + drweb_vnum, drweb_slen, drweb_fin = 0x0000; + unsigned long bread; + uschar hostname[256]; + struct hostent *he; + struct in_addr in; + pcre *drweb_re; + + if ((drweb_options = string_nextinlist(&av_scanner_work, &sep, + drweb_options_buffer, sizeof(drweb_options_buffer))) == NULL) { + /* no options supplied, use default options */ + drweb_options = drweb_options_default; + }; + + if (*drweb_options != '/') { + + /* extract host and port part */ + if( sscanf(CS drweb_options, "%s %u", hostname, &port) != 2 ) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: drweb: invalid socket '%s'", drweb_options); + return DEFER; + } + + /* Lookup the host */ + if((he = gethostbyname(CS hostname)) == 0) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: drweb: failed to lookup host '%s'", hostname); + return DEFER; + } + + in = *(struct in_addr *) he->h_addr_list[0]; + + /* Open the drwebd TCP socket */ + if ( (sock = ip_socket(SOCK_STREAM, AF_INET)) < 0) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: drweb: unable to acquire socket (%s)", + strerror(errno)); + return DEFER; + } + + if (ip_connect(sock, AF_INET, (uschar*)inet_ntoa(in), port, 5) < 0) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: drweb: connection to %s, port %u failed (%s)", + inet_ntoa(in), port, strerror(errno)); + return DEFER; + } + + /* prepare variables */ + drweb_cmd = htonl(DRWEBD_SCAN_CMD); + drweb_flags = htonl(DRWEBD_RETURN_VIRUSES | DRWEBD_IS_MAIL); + snprintf(CS scanrequest, 1024,CS"%s/scan/%s/%s.eml", + spool_directory, message_id, message_id); + + /* calc file size */ + drweb_fd = open(CS scanrequest, O_RDONLY); + if (drweb_fd == -1) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: drweb: can't open spool file %s: %s", + scanrequest, strerror(errno)); + return DEFER; + } + fsize = lseek(drweb_fd, 0, SEEK_END); + if (fsize == -1) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: drweb: can't seek spool file %s: %s", + scanrequest, strerror(errno)); + return DEFER; + } + drweb_slen = htonl(fsize); + lseek(drweb_fd, 0, SEEK_SET); + + /* send scan request */ + if ((send(sock, &drweb_cmd, sizeof(drweb_cmd), 0) < 0) || + (send(sock, &drweb_flags, sizeof(drweb_flags), 0) < 0) || + (send(sock, &drweb_fin, sizeof(drweb_fin), 0) < 0) || + (send(sock, &drweb_slen, sizeof(drweb_slen), 0) < 0)) { + close(sock); + close(drweb_fd); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: drweb: unable to send commands to socket (%s)", drweb_options); + return DEFER; + } + + drweb_fbuf = (uschar *) malloc (fsize); + if (!drweb_fbuf) { + close(sock); + close(drweb_fd); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: drweb: unable to allocate memory %u for file (%s)", + fsize, scanrequest); + return DEFER; + } + + result = read (drweb_fd, drweb_fbuf, fsize); + if (result == -1) { + close(sock); + close(drweb_fd); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: drweb: can't read spool file %s: %s", + scanrequest, strerror(errno)); + return DEFER; + } + + /* send file body to socket */ + if (send(sock, drweb_fbuf, fsize, 0) < 0) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: drweb: unable to send file body to socket (%s)", drweb_options); + return DEFER; + } + close(drweb_fd); + free(drweb_fbuf); + } + else { + /* open the drwebd UNIX socket */ + sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock < 0) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: drweb: can't open UNIX socket"); + return DEFER; + } + server.sun_family = AF_UNIX; + Ustrcpy(server.sun_path, drweb_options); + if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: drweb: unable to connect to socket (%s). errno=%d", drweb_options, errno); + return DEFER; + } + + /* prepare variables */ + drweb_cmd = htonl(DRWEBD_SCAN_CMD); + drweb_flags = htonl(DRWEBD_RETURN_VIRUSES | DRWEBD_IS_MAIL); + snprintf(CS scanrequest, 1024,CS"%s/scan/%s/%s.eml", spool_directory, message_id, message_id); + drweb_slen = htonl(Ustrlen(scanrequest)); + + /* send scan request */ + if ((send(sock, &drweb_cmd, sizeof(drweb_cmd), 0) < 0) || + (send(sock, &drweb_flags, sizeof(drweb_flags), 0) < 0) || + (send(sock, &drweb_slen, sizeof(drweb_slen), 0) < 0) || + (send(sock, scanrequest, Ustrlen(scanrequest), 0) < 0) || + (send(sock, &drweb_fin, sizeof(drweb_fin), 0) < 0)) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: drweb: unable to send commands to socket (%s)", drweb_options); + return DEFER; + } + } + + /* wait for result */ + if ((bread = recv(sock, &drweb_rc, sizeof(drweb_rc), 0) != sizeof(drweb_rc))) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: drweb: unable to read return code"); + return DEFER; + } + drweb_rc = ntohl(drweb_rc); + + if ((bread = recv(sock, &drweb_vnum, sizeof(drweb_vnum), 0) != sizeof(drweb_vnum))) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: drweb: unable to read the number of viruses"); + return DEFER; + } + drweb_vnum = ntohl(drweb_vnum); + + /* "virus(es) found" if virus number is > 0 */ + if (drweb_vnum) + { + int i; + uschar pre_malware_nb[256]; + + malware_name = malware_name_buffer; + + /* setup default virus name */ + Ustrcpy(malware_name_buffer,"unknown"); + + /* read and concatenate virus names into one string */ + for (i=0;i= 2) { + pcre_copy_substring(CS tmpbuf, ovector, result, 1, CS pre_malware_nb, 255); + } + /* the first name we just copy to malware_name */ + if (i==0) + Ustrcpy(CS malware_name_buffer, CS pre_malware_nb); + else { + /* concatenate each new virus name to previous */ + int slen = Ustrlen(malware_name_buffer); + if (slen < (slen+Ustrlen(pre_malware_nb))) { + Ustrcat(malware_name_buffer, "/"); + Ustrcat(malware_name_buffer, pre_malware_nb); + } + } + } + } + else { + /* no virus found */ + malware_name = NULL; + }; + close(sock); + } + /* ----------------------------------------------------------------------- */ + else if (strcmpic(scanner_name,US"aveserver") == 0) { + uschar *kav_options; + uschar kav_options_buffer[1024]; + uschar kav_options_default[] = "/var/run/aveserver"; + uschar buf[32768]; + uschar *p; + struct sockaddr_un server; + int sock; + + if ((kav_options = string_nextinlist(&av_scanner_work, &sep, + kav_options_buffer, + sizeof(kav_options_buffer))) == NULL) { + /* no options supplied, use default options */ + kav_options = kav_options_default; + }; + + /* open the aveserver socket */ + sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock < 0) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: can't open UNIX socket."); + return DEFER; + } + server.sun_family = AF_UNIX; + Ustrcpy(server.sun_path, kav_options); + if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: unable to connect to aveserver UNIX socket (%s). errno=%d", kav_options, errno); + return DEFER; + } + + /* read aveserver's greeting and see if it is ready (2xx greeting) */ + recv_line(sock, buf, 32768); + + if (buf[0] != '2') { + /* aveserver is having problems */ + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: aveserver is unavailable (Responded: %s).", ((buf[0] != 0) ? buf : (uschar *)"nothing") ); + return DEFER; + }; + + /* prepare our command */ + snprintf(CS buf, 32768, "SCAN bPQRSTUW %s/scan/%s/%s.eml\r\n", spool_directory, message_id, message_id); + + /* and send it */ + if (send(sock, buf, Ustrlen(buf), 0) < 0) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: unable to write to aveserver UNIX socket (%s)", kav_options); + return DEFER; + } + + malware_name = NULL; + /* read response lines, find malware name and final response */ + while (recv_line(sock, buf, 32768) > 0) { + debug_printf("aveserver: %s\n", buf); + if (buf[0] == '2') break; + if (Ustrncmp(buf,"322",3) == 0) { + uschar *p = Ustrchr(&buf[4],' '); + *p = '\0'; + Ustrcpy(malware_name_buffer,&buf[4]); + malware_name = malware_name_buffer; + }; + } + + close(sock); + } + /* "fsecure" scanner type ------------------------------------------------- */ + else if (strcmpic(scanner_name,US"fsecure") == 0) { + uschar *fsecure_options; + uschar fsecure_options_buffer[1024]; + uschar fsecure_options_default[] = "/var/run/.fsav"; + struct sockaddr_un server; + int sock, i, j, bread = 0; + uschar file_name[1024]; + uschar av_buffer[1024]; + pcre *fs_inf; + static uschar *cmdoptions[] = { "CONFIGURE\tARCHIVE\t1\n","CONFIGURE\tTIMEOUT\t0\n","CONFIGURE\tMAXARCH\t5\n","CONFIGURE\tMIME\t1\n" }; + + malware_name = NULL; + if ((fsecure_options = string_nextinlist(&av_scanner_work, &sep, + fsecure_options_buffer, + sizeof(fsecure_options_buffer))) == NULL) { + /* no options supplied, use default options */ + fsecure_options = fsecure_options_default; + }; + + /* open the fsecure socket */ + sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock < 0) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: unable to open fsecure socket %s (%s)", + fsecure_options, strerror(errno)); + return DEFER; + } + server.sun_family = AF_UNIX; + Ustrcpy(server.sun_path, fsecure_options); + if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: unable to connect to fsecure socket %s (%s)", + fsecure_options, strerror(errno)); + return DEFER; + } + + /* pass options */ + memset(av_buffer, 0, sizeof(av_buffer)); + for (i=0; i != 4; i++) { + /* debug_printf("send option \"%s\"",cmdoptions[i]); */ + if (write(sock, cmdoptions[i], Ustrlen(cmdoptions[i])) < 0) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: unable to write fsecure option %d to %s (%s)", + i, fsecure_options, strerror(errno)); + return DEFER; + }; + + bread = read(sock, av_buffer, sizeof(av_buffer)); + if (bread >0) av_buffer[bread]='\0'; + if (bread < 0) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: unable to read fsecure answer %d (%s)", i, strerror(errno)); + return DEFER; + }; + for (j=0;j= 2) { + /* Got it */ + pcre_copy_substring(CS av_buffer, ovector, i, 1, CS malware_name_buffer, 255); + malware_name = malware_name_buffer; + }; + }; + } + while (Ustrstr(av_buffer, "OK\tScan ok.") == NULL); + close(sock); + } + /* ----------------------------------------------------------------------- */ + + /* "kavdaemon" scanner type ------------------------------------------------ */ + else if (strcmpic(scanner_name,US"kavdaemon") == 0) { + uschar *kav_options; + uschar kav_options_buffer[1024]; + uschar kav_options_default[] = "/var/run/AvpCtl"; + struct sockaddr_un server; + int sock; + time_t t; + uschar tmpbuf[1024]; + uschar scanrequest[1024]; + uschar kav_match_string[128]; + int kav_rc; + unsigned long kav_reportlen, bread; + pcre *kav_re; + + if ((kav_options = string_nextinlist(&av_scanner_work, &sep, + kav_options_buffer, + sizeof(kav_options_buffer))) == NULL) { + /* no options supplied, use default options */ + kav_options = kav_options_default; + }; + + /* open the kavdaemon socket */ + sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock < 0) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: can't open UNIX socket."); + return DEFER; + } + server.sun_family = AF_UNIX; + Ustrcpy(server.sun_path, kav_options); + if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: unable to connect to kavdaemon UNIX socket (%s). errno=%d", kav_options, errno); + return DEFER; + } + + /* get current date and time, build scan request */ + time(&t); + strftime(CS tmpbuf, sizeof(tmpbuf), "<0>%d %b %H:%M:%S:%%s/scan/%%s", localtime(&t)); + snprintf(CS scanrequest, 1024,CS tmpbuf, spool_directory, message_id); + + /* send scan request */ + if (send(sock, scanrequest, Ustrlen(scanrequest)+1, 0) < 0) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: unable to write to kavdaemon UNIX socket (%s)", kav_options); + return DEFER; + } + + /* wait for result */ + if ((bread = recv(sock, tmpbuf, 2, 0) != 2)) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: unable to read 2 bytes from kavdaemon socket."); + return DEFER; + } + + /* get errorcode from one nibble */ + if (test_byte_order() == LITTLE_MY_ENDIAN) { + kav_rc = tmpbuf[0] & 0x0F; + } + else { + kav_rc = tmpbuf[1] & 0x0F; + }; + + /* improper kavdaemon configuration */ + if ( (kav_rc == 5) || (kav_rc == 6) ) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: please reconfigure kavdaemon to NOT disinfect or remove infected files."); + return DEFER; + }; + + if (kav_rc == 1) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: kavdaemon reported 'scanning not completed' (code 1)."); + return DEFER; + }; + + if (kav_rc == 7) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: kavdaemon reported 'kavdaemon damaged' (code 7)."); + return DEFER; + }; + + /* code 8 is not handled, since it is ambigous. It appears mostly on + bounces where part of a file has been cut off */ + + /* "virus found" return codes (2-4) */ + if ((kav_rc > 1) && (kav_rc < 5)) { + int report_flag = 0; + + /* setup default virus name */ + Ustrcpy(malware_name_buffer,"unknown"); + malware_name = malware_name_buffer; + + if (test_byte_order() == LITTLE_MY_ENDIAN) { + report_flag = tmpbuf[1]; + } + else { + report_flag = tmpbuf[0]; + }; + + /* read the report, if available */ + if( report_flag == 1 ) { + /* read report size */ + if ((bread = recv(sock, &kav_reportlen, 4, 0)) != 4) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: cannot read report size from kavdaemon"); + return DEFER; + }; + + /* it's possible that avp returns av_buffer[1] == 1 but the + reportsize is 0 (!?) */ + if (kav_reportlen > 0) { + /* set up match regex, depends on retcode */ + if( kav_rc == 3 ) + Ustrcpy(kav_match_string, "suspicion:\\s*(.+?)\\s*$"); + else + Ustrcpy(kav_match_string, "infected:\\s*(.+?)\\s*$"); + + kav_re = pcre_compile( CS kav_match_string, + PCRE_COPT, + (const char **)&rerror, + &roffset, + NULL ); + + /* read report, linewise */ + while (kav_reportlen > 0) { + int result = 0; + int ovector[30]; + + bread = 0; + while ( recv(sock, &tmpbuf[bread], 1, 0) == 1 ) { + kav_reportlen--; + if ( (tmpbuf[bread] == '\n') || (bread > 1021) ) break; + bread++; + }; + bread++; + tmpbuf[bread] = '\0'; + + /* try matcher on the line, grab substring */ + result = pcre_exec(kav_re, NULL, CS tmpbuf, Ustrlen(tmpbuf), 0, 0, ovector, 30); + if (result >= 2) { + pcre_copy_substring(CS tmpbuf, ovector, result, 1, CS malware_name_buffer, 255); + break; + }; + }; + }; + }; + } + else { + /* no virus found */ + malware_name = NULL; + }; + + close(sock); + } + /* ----------------------------------------------------------------------- */ + + + /* "cmdline" scanner type ------------------------------------------------ */ + else if (strcmpic(scanner_name,US"cmdline") == 0) { + uschar *cmdline_scanner; + uschar cmdline_scanner_buffer[1024]; + uschar *cmdline_trigger; + uschar cmdline_trigger_buffer[1024]; + const pcre *cmdline_trigger_re; + uschar *cmdline_regex; + uschar cmdline_regex_buffer[1024]; + const pcre *cmdline_regex_re; + uschar file_name[1024]; + uschar commandline[1024]; + void (*eximsigchld)(int); + void (*eximsigpipe)(int); + FILE *scanner_out = NULL; + FILE *scanner_record = NULL; + uschar linebuffer[32767]; + int trigger = 0; + int result; + int ovector[30]; + + /* find scanner command line */ + if ((cmdline_scanner = string_nextinlist(&av_scanner_work, &sep, + cmdline_scanner_buffer, + sizeof(cmdline_scanner_buffer))) == NULL) { + /* no command line supplied */ + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: missing commandline specification for cmdline scanner type."); + return DEFER; + }; + + /* find scanner output trigger */ + if ((cmdline_trigger = string_nextinlist(&av_scanner_work, &sep, + cmdline_trigger_buffer, + sizeof(cmdline_trigger_buffer))) == NULL) { + /* no trigger regex supplied */ + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: missing trigger specification for cmdline scanner type."); + return DEFER; + }; + + /* precompile trigger regex */ + cmdline_trigger_re = pcre_compile(CS cmdline_trigger, PCRE_COPT, (const char **)&rerror, &roffset, NULL); + if (cmdline_trigger_re == NULL) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: regular expression error in '%s': %s at offset %d", cmdline_trigger_re, rerror, roffset); + return DEFER; + }; + + /* find scanner name regex */ + if ((cmdline_regex = string_nextinlist(&av_scanner_work, &sep, + cmdline_regex_buffer, + sizeof(cmdline_regex_buffer))) == NULL) { + /* no name regex supplied */ + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: missing virus name regex specification for cmdline scanner type."); + return DEFER; + }; + + /* precompile name regex */ + cmdline_regex_re = pcre_compile(CS cmdline_regex, PCRE_COPT, (const char **)&rerror, &roffset, NULL); + if (cmdline_regex_re == NULL) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: regular expression error in '%s': %s at offset %d", cmdline_regex_re, rerror, roffset); + return DEFER; + }; + + /* prepare scanner call */ + snprintf(CS file_name,1024,"%s/scan/%s", spool_directory, message_id); + snprintf(CS commandline,1024, CS cmdline_scanner,file_name); + /* redirect STDERR too */ + Ustrcat(commandline," 2>&1"); + + /* store exims signal handlers */ + eximsigchld = signal(SIGCHLD,SIG_DFL); + eximsigpipe = signal(SIGPIPE,SIG_DFL); + + scanner_out = popen(CS commandline,"r"); + if (scanner_out == NULL) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: calling cmdline scanner (%s) failed: %s.", commandline, strerror(errno)); + signal(SIGCHLD,eximsigchld); + signal(SIGPIPE,eximsigpipe); + return DEFER; + }; + + snprintf(CS file_name,1024,"%s/scan/%s/%s_scanner_output", spool_directory, message_id, message_id); + scanner_record = fopen(CS file_name,"w"); + + if (scanner_record == NULL) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: opening scanner output file (%s) failed: %s.", file_name, strerror(errno)); + pclose(scanner_out); + signal(SIGCHLD,eximsigchld); + signal(SIGPIPE,eximsigpipe); + return DEFER; + }; + + /* look for trigger while recording output */ + while(fgets(CS linebuffer,32767,scanner_out) != NULL) { + if ( Ustrlen(linebuffer) > fwrite(linebuffer, 1, Ustrlen(linebuffer), scanner_record) ) { + /* short write */ + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: short write on scanner output file (%s).", file_name); + pclose(scanner_out); + signal(SIGCHLD,eximsigchld); + signal(SIGPIPE,eximsigpipe); + return DEFER; + }; + /* try trigger match */ + if (!trigger && regex_match_and_setup(cmdline_trigger_re, linebuffer, 0, -1)) + trigger = 1; + }; + + fclose(scanner_record); + pclose(scanner_out); + signal(SIGCHLD,eximsigchld); + signal(SIGPIPE,eximsigpipe); + + if (trigger) { + /* setup default virus name */ + Ustrcpy(malware_name_buffer,"unknown"); + malware_name = malware_name_buffer; + + /* re-open the scanner output file, look for name match */ + scanner_record = fopen(CS file_name,"r"); + while(fgets(CS linebuffer,32767,scanner_record) != NULL) { + /* try match */ + result = pcre_exec(cmdline_regex_re, NULL, CS linebuffer, Ustrlen(linebuffer), 0, 0, ovector, 30); + if (result >= 2) { + pcre_copy_substring(CS linebuffer, ovector, result, 1, CS malware_name_buffer, 255); + }; + }; + fclose(scanner_record); + } + else { + /* no virus found */ + malware_name = NULL; + }; + } + /* ----------------------------------------------------------------------- */ + + + /* "sophie" scanner type ------------------------------------------------- */ + else if (strcmpic(scanner_name,US"sophie") == 0) { + uschar *sophie_options; + uschar sophie_options_buffer[1024]; + uschar sophie_options_default[] = "/var/run/sophie"; + int bread = 0; + struct sockaddr_un server; + int sock; + uschar file_name[1024]; + uschar av_buffer[1024]; + + if ((sophie_options = string_nextinlist(&av_scanner_work, &sep, + sophie_options_buffer, + sizeof(sophie_options_buffer))) == NULL) { + /* no options supplied, use default options */ + sophie_options = sophie_options_default; + }; + + /* open the sophie socket */ + sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock < 0) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: can't open UNIX socket."); + return DEFER; + } + server.sun_family = AF_UNIX; + Ustrcpy(server.sun_path, sophie_options); + if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: unable to connect to sophie UNIX socket (%s). errno=%d", sophie_options, errno); + return DEFER; + } + + /* pass the scan directory to sophie */ + snprintf(CS file_name,1024,"%s/scan/%s", spool_directory, message_id); + if (write(sock, file_name, Ustrlen(file_name)) < 0) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: unable to write to sophie UNIX socket (%s)", sophie_options); + return DEFER; + }; + + write(sock, "\n", 1); + + /* wait for result */ + memset(av_buffer, 0, sizeof(av_buffer)); + if ((!(bread = read(sock, av_buffer, sizeof(av_buffer))) > 0)) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: unable to read from sophie UNIX socket (%s)", sophie_options); + return DEFER; + }; + + close(sock); + + /* infected ? */ + if (av_buffer[0] == '1') { + if (Ustrchr(av_buffer, '\n')) *Ustrchr(av_buffer, '\n') = '\0'; + Ustrcpy(malware_name_buffer,&av_buffer[2]); + malware_name = malware_name_buffer; + } + else if (!strncmp(CS av_buffer, "-1", 2)) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: malware acl condition: sophie reported error"); + return DEFER; + } + else { + /* all ok, no virus */ + malware_name = NULL; + }; + } + /* ----------------------------------------------------------------------- */ + + + /* "clamd" scanner type ------------------------------------------------- */ + /* This code was contributed by David Saez */ + else if (strcmpic(scanner_name,US"clamd") == 0) { + uschar *clamd_options; + uschar clamd_options_buffer[1024]; + uschar clamd_options_default[] = "/tmp/clamd"; + uschar *p,*vname; + struct sockaddr_un server; + int sock,port,bread=0; + uschar file_name[1024]; + uschar av_buffer[1024]; + uschar hostname[256]; + struct hostent *he; + struct in_addr in; + + if ((clamd_options = string_nextinlist(&av_scanner_work, &sep, + clamd_options_buffer, + sizeof(clamd_options_buffer))) == NULL) { + /* no options supplied, use default options */ + clamd_options = clamd_options_default; + } + + /* socket does not start with '/' -> network socket */ + if (*clamd_options != '/') { + + /* extract host and port part */ + if( sscanf(CS clamd_options, "%s %u", hostname, &port) != 2 ) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: clamd: invalid socket '%s'", clamd_options); + return DEFER; + }; + + /* Lookup the host */ + if((he = gethostbyname(CS hostname)) == 0) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: clamd: failed to lookup host '%s'", hostname); + return DEFER; + } + + in = *(struct in_addr *) he->h_addr_list[0]; + + /* Open the ClamAV Socket */ + if ( (sock = ip_socket(SOCK_STREAM, AF_INET)) < 0) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: clamd: unable to acquire socket (%s)", + strerror(errno)); + return DEFER; + } + + if (ip_connect(sock, AF_INET, (uschar*)inet_ntoa(in), port, 5) < 0) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: clamd: connection to %s, port %u failed (%s)", + inet_ntoa(in), port, strerror(errno)); + return DEFER; + } + } + else { + /* open the local socket */ + if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: clamd: unable to acquire socket (%s)", + strerror(errno)); + return DEFER; + } + + server.sun_family = AF_UNIX; + Ustrcpy(server.sun_path, clamd_options); + + if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: clamd: unable to connect to UNIX socket %s (%s)", + clamd_options, strerror(errno) ); + return DEFER; + } + } + + /* Pass the string to ClamAV (7 = "SCAN \n" + \0) */ + + snprintf(CS file_name,1024,"SCAN %s/scan/%s\n", spool_directory, message_id); + + if (send(sock, file_name, Ustrlen(file_name), 0) < 0) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC,"malware acl condition: clamd: unable to write to socket (%s)", + strerror(errno)); + return DEFER; + } + + /* + We're done sending, close socket for writing. + + One user reported that clamd 0.70 does not like this any more ... + + */ + + /* shutdown(sock, SHUT_WR); */ + + /* Read the result */ + memset(av_buffer, 0, sizeof(av_buffer)); + bread = read(sock, av_buffer, sizeof(av_buffer)); + close(sock); + + if (!(bread > 0)) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: clamd: unable to read from socket (%s)", + strerror(errno)); + return DEFER; + } + + if (bread == sizeof(av_buffer)) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: clamd: buffer too small"); + return DEFER; + } + + /* Check the result. ClamAV Returns + infected: -> ": FOUND" + not-infected: -> ": OK" + error: -> ": ERROR */ + + if (!(*av_buffer)) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: clamd: ClamAV returned null"); + return DEFER; + } + + /* colon in returned output? */ + if((p = Ustrrchr(av_buffer,':')) == NULL) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: clamd: ClamAV returned malformed result: %s", + av_buffer); + return DEFER; + } + + /* strip filename strip CR at the end */ + ++p; + while (*p == ' ') ++p; + vname = p; + p = vname + Ustrlen(vname) - 1; + if( *p == '\n' ) *p = '\0'; + + if ((p = Ustrstr(vname, "FOUND"))!=NULL) { + *p=0; + for (--p;p>vname && *p<=32;p--) *p=0; + for (;*vname==32;vname++); + Ustrcpy(malware_name_buffer,vname); + malware_name = malware_name_buffer; + } + else { + if (Ustrstr(vname, "ERROR")!=NULL) { + /* ClamAV reports ERROR + Find line start */ + for (;*vname!='\n' && vname>av_buffer; vname--); + if (*vname=='\n') vname++; + + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: clamd: ClamAV returned %s",vname); + return DEFER; + } + else { + /* Everything should be OK */ + malware_name = NULL; + } + } + } + /* ----------------------------------------------------------------------- */ + + + /* "mksd" scanner type --------------------------------------------------- */ + else if (strcmpic(scanner_name,US"mksd") == 0) { + uschar *mksd_options; + char *mksd_options_end; + uschar mksd_options_buffer[32]; + int mksd_maxproc = 1; /* default, if no option supplied */ + struct sockaddr_un server; + int sock; + int retval; + + if ((mksd_options = string_nextinlist(&av_scanner_work, &sep, + mksd_options_buffer, + sizeof(mksd_options_buffer))) != NULL) { + mksd_maxproc = (int) strtol(CS mksd_options, &mksd_options_end, 10); + if ((*mksd_options == '\0') || (*mksd_options_end != '\0') || + (mksd_maxproc < 1) || (mksd_maxproc > 32)) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: mksd: invalid option '%s'", mksd_options); + return DEFER; + } + } + + /* open the mksd socket */ + sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock < 0) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: can't open UNIX socket."); + return DEFER; + } + server.sun_family = AF_UNIX; + Ustrcpy(server.sun_path, "/var/run/mksd/socket"); + if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) { + close(sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: unable to connect to mksd UNIX socket (/var/run/mksd/socket). errno=%d", errno); + return DEFER; + } + + malware_name = NULL; + + /* choose the appropriate scan routine */ + retval = demime_ok ? + mksd_scan_unpacked(sock, mksd_maxproc) : + mksd_scan_packed(sock); + + if (retval != OK) + return retval; + } + /* ----------------------------------------------------------------------- */ + + /* "unknown" scanner type ------------------------------------------------- */ + else { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware condition: unknown scanner type '%s'", scanner_name); + return DEFER; + }; + /* ----------------------------------------------------------------------- */ + + /* set "been here, done that" marker */ + malware_ok = 1; + }; + + /* match virus name against pattern (caseless ------->----------v) */ + if ( (malware_name != NULL) && + (regex_match_and_setup(re, malware_name, 0, -1)) ) { + return OK; + } + else { + return FAIL; + }; +} + + +/* simple wrapper for reading lines from sockets */ +int recv_line(int sock, uschar *buffer, int size) { + uschar *p = buffer; + + memset(buffer,0,size); + /* read until \n */ + while(recv(sock,p,1,0) > -1) { + if ((p-buffer) > (size-2)) break; + if (*p == '\n') break; + if (*p != '\r') p++; + }; + *p = '\0'; + + return (p-buffer); +} + + +/* ============= private routines for the "mksd" scanner type ============== */ + +#include + +int mksd_writev (int sock, struct iovec *iov, int iovcnt) +{ + int i; + + for (;;) { + do + i = writev (sock, iov, iovcnt); + while ((i < 0) && (errno == EINTR)); + if (i <= 0) { + close (sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: unable to write to mksd UNIX socket (/var/run/mksd/socket)"); + return -1; + } + + for (;;) + if (i >= iov->iov_len) { + if (--iovcnt == 0) + return 0; + i -= iov->iov_len; + iov++; + } else { + iov->iov_len -= i; + iov->iov_base = CS iov->iov_base + i; + break; + } + } +} + +int mksd_read_lines (int sock, uschar *av_buffer, int av_buffer_size) +{ + int offset = 0; + int i; + + do { + if ((i = recv (sock, av_buffer+offset, av_buffer_size-offset, 0)) <= 0) { + close (sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: unable to read from mksd UNIX socket (/var/run/mksd/socket)"); + return -1; + } + + offset += i; + /* offset == av_buffer_size -> buffer full */ + if (offset == av_buffer_size) { + close (sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: malformed reply received from mksd"); + return -1; + } + } while (av_buffer[offset-1] != '\n'); + + av_buffer[offset] = '\0'; + return offset; +} + +int mksd_parse_line (char *line) +{ + char *p; + + switch (*line) { + case 'O': + /* OK */ + return OK; + case 'E': + case 'A': + /* ERR */ + if ((p = strchr (line, '\n')) != NULL) + (*p) = '\0'; + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: mksd scanner failed: %s", line); + return DEFER; + default: + /* VIR */ + if ((p = strchr (line, '\n')) != NULL) { + (*p) = '\0'; + if (((p-line) > 5) && ((p-line) < sizeof (malware_name_buffer)) && (line[3] == ' ')) + if (((p = strchr (line+4, ' ')) != NULL) && ((p-line) > 4)) { + (*p) = '\0'; + Ustrcpy (malware_name_buffer, line+4); + malware_name = malware_name_buffer; + return OK; + } + } + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: malformed reply received from mksd: %s", line); + return DEFER; + } +} + +int mksd_scan_packed (int sock) +{ + struct iovec iov[7]; + char *cmd = "MSQ/scan/.eml\n"; + uschar av_buffer[1024]; + + iov[0].iov_base = cmd; + iov[0].iov_len = 3; + iov[1].iov_base = CS spool_directory; + iov[1].iov_len = Ustrlen (spool_directory); + iov[2].iov_base = cmd + 3; + iov[2].iov_len = 6; + iov[3].iov_base = iov[5].iov_base = CS message_id; + iov[3].iov_len = iov[5].iov_len = Ustrlen (message_id); + iov[4].iov_base = cmd + 3; + iov[4].iov_len = 1; + iov[6].iov_base = cmd + 9; + iov[6].iov_len = 5; + + if (mksd_writev (sock, iov, 7) < 0) + return DEFER; + + if (mksd_read_lines (sock, av_buffer, sizeof (av_buffer)) < 0) + return DEFER; + + close (sock); + + return mksd_parse_line (CS av_buffer); +} + +int mksd_scan_unpacked (int sock, int maxproc) +{ + struct iovec iov[5]; + char *cmd = "\nSQ/"; + DIR *unpdir; + struct dirent *entry; + int pending = 0; + uschar *line; + int i, offset; + uschar mbox_name[1024]; + uschar unpackdir[1024]; + uschar av_buffer[16384]; + + snprintf (CS mbox_name, sizeof (mbox_name), "%s.eml", CS message_id); + snprintf (CS unpackdir, sizeof (unpackdir), "%s/scan/%s", CS spool_directory, CS message_id); + + if ((unpdir = opendir (CS unpackdir)) == NULL) { + close (sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: unable to scan spool directory"); + return DEFER; + } + + iov[0].iov_base = cmd; + iov[0].iov_len = 3; + iov[1].iov_base = CS unpackdir; + iov[1].iov_len = Ustrlen (unpackdir); + iov[2].iov_base = cmd + 3; + iov[2].iov_len = 1; + iov[4].iov_base = cmd; + iov[4].iov_len = 1; + + /* main loop */ + while ((unpdir != NULL) || (pending > 0)) { + + /* write loop */ + while ((pending < maxproc) && (unpdir != NULL)) { + if ((entry = readdir (unpdir)) != NULL) { + if ((Ustrcmp (entry->d_name, ".") != 0) && + (Ustrcmp (entry->d_name, "..") != 0) && + (Ustrcmp (entry->d_name, mbox_name) != 0)) { + iov[3].iov_base = entry->d_name; + iov[3].iov_len = strlen (entry->d_name); + if (mksd_writev (sock, iov, 5) < 0) { + closedir (unpdir); + return DEFER; + } + iov[0].iov_base = cmd + 1; + iov[0].iov_len = 2; + pending++; + } + } else { + closedir (unpdir); + unpdir = NULL; + } + } + + /* read and parse */ + if (pending > 0) { + if ((offset = mksd_read_lines (sock, av_buffer, sizeof (av_buffer))) < 0) { + if (unpdir != NULL) + closedir (unpdir); + return DEFER; + } + line = av_buffer; + do { + if (((i = mksd_parse_line (CS line)) != OK) || (malware_name != NULL)) { + close (sock); + if (unpdir != NULL) + closedir (unpdir); + return i; + } + pending--; + if ((line = Ustrchr (line, '\n')) == NULL) { + close (sock); + if (unpdir != NULL) + closedir (unpdir); + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: unterminated line received from mksd"); + return DEFER; + } + } while (++line != (av_buffer + offset)); + offset = 0; + } + } + + close (sock); + return OK; +} + diff --git a/src/src/mime.c b/src/src/mime.c new file mode 100644 index 000000000..810420431 --- /dev/null +++ b/src/src/mime.c @@ -0,0 +1,714 @@ +/* $Cambridge: exim/src/src/mime.c,v 1.1.2.1 2004/11/26 09:13:34 tom Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* This file is part of the exiscan-acl content scanner +patch. It is NOT part of the standard exim distribution. */ + +/* Copyright (c) Tom Kistner 2004 */ +/* License: GPL */ + +#include "exim.h" +#include "mime.h" +#include + +FILE *mime_stream = NULL; +uschar *mime_current_boundary = NULL; + + +/************************************************* +* decode quoted-printable chars * +*************************************************/ + +/* gets called when we hit a = + returns: new pointer position + result code in c: + -2 - decode error + -1 - soft line break, no char + 0-255 - char to write +*/ + +unsigned int mime_qp_hstr_i(uschar *cptr) { + unsigned int i, j = 0; + while (cptr && *cptr && isxdigit(*cptr)) { + i = *cptr++ - '0'; + if (9 < i) i -= 7; + j <<= 4; + j |= (i & 0x0f); + } + return(j); +} + +uschar *mime_decode_qp_char(uschar *qp_p,int *c) { + uschar hex[] = {0,0,0}; + int nan = 0; + uschar *initial_pos = qp_p; + + /* advance one char */ + qp_p++; + + REPEAT_FIRST: + if ( (*qp_p == '\t') || (*qp_p == ' ') || (*qp_p == '\r') ) { + /* tab or whitespace may follow + just ignore it, but remember + that this is not a valid hex + encoding any more */ + nan = 1; + qp_p++; + goto REPEAT_FIRST; + } + else if ( (('0' <= *qp_p) && (*qp_p <= '9')) || (('A' <= *qp_p) && (*qp_p <= 'F')) || (('a' <= *qp_p) && (*qp_p <= 'f')) ) { + /* this is a valid hex char, if nan is unset */ + if (nan) { + /* this is illegal */ + *c = -2; + return initial_pos; + } + else { + hex[0] = *qp_p; + qp_p++; + }; + } + else if (*qp_p == '\n') { + /* hit soft line break already, continue */ + *c = -1; + return qp_p; + } + else { + /* illegal char here */ + *c = -2; + return initial_pos; + }; + + if ( (('0' <= *qp_p) && (*qp_p <= '9')) || (('A' <= *qp_p) && (*qp_p <= 'F')) || (('a' <= *qp_p) && (*qp_p <= 'f')) ) { + if (hex[0] > 0) { + hex[1] = *qp_p; + /* do hex conversion */ + *c = mime_qp_hstr_i(hex); + qp_p++; + return qp_p; + } + else { + /* huh ? */ + *c = -2; + return initial_pos; + }; + } + else { + /* illegal char */ + *c = -2; + return initial_pos; + }; +} + + +uschar *mime_parse_line(uschar *buffer, uschar *encoding, int *num_decoded) { + uschar *data = NULL; + + data = (uschar *)malloc(Ustrlen(buffer)+2); + + if (encoding == NULL) { + /* no encoding type at all */ + NO_DECODING: + memcpy(data, buffer, Ustrlen(buffer)); + data[(Ustrlen(buffer))] = 0; + *num_decoded = Ustrlen(data); + return data; + } + else if (Ustrcmp(encoding,"base64") == 0) { + uschar *p = buffer; + int offset = 0; + + /* ----- BASE64 ---------------------------------------------------- */ + /* NULL out '\r' and '\n' chars */ + while (Ustrrchr(p,'\r') != NULL) { + *(Ustrrchr(p,'\r')) = '\0'; + }; + while (Ustrrchr(p,'\n') != NULL) { + *(Ustrrchr(p,'\n')) = '\0'; + }; + + while (*(p+offset) != '\0') { + /* hit illegal char ? */ + if (mime_b64[*(p+offset)] == 128) { + offset++; + } + else { + *p = mime_b64[*(p+offset)]; + p++; + }; + }; + *p = 255; + + /* line is translated, start bit shifting */ + p = buffer; + *num_decoded = 0; + while(*p != 255) { + uschar tmp_c; + + /* byte 0 ---------------------- */ + if (*(p+1) == 255) { + break; + } + data[(*num_decoded)] = *p; + data[(*num_decoded)] <<= 2; + tmp_c = *(p+1); + tmp_c >>= 4; + data[(*num_decoded)] |= tmp_c; + (*num_decoded)++; + p++; + /* byte 1 ---------------------- */ + if (*(p+1) == 255) { + break; + } + data[(*num_decoded)] = *p; + data[(*num_decoded)] <<= 4; + tmp_c = *(p+1); + tmp_c >>= 2; + data[(*num_decoded)] |= tmp_c; + (*num_decoded)++; + p++; + /* byte 2 ---------------------- */ + if (*(p+1) == 255) { + break; + } + data[(*num_decoded)] = *p; + data[(*num_decoded)] <<= 6; + data[(*num_decoded)] |= *(p+1); + (*num_decoded)++; + p+=2; + + }; + return data; + /* ----------------------------------------------------------------- */ + } + else if (Ustrcmp(encoding,"quoted-printable") == 0) { + uschar *p = buffer; + + /* ----- QP -------------------------------------------------------- */ + *num_decoded = 0; + while (*p != 0) { + if (*p == '=') { + int decode_qp_result; + + p = mime_decode_qp_char(p,&decode_qp_result); + + if (decode_qp_result == -2) { + /* Error from decoder. p is unchanged. */ + data[(*num_decoded)] = '='; + (*num_decoded)++; + p++; + } + else if (decode_qp_result == -1) { + break; + } + else if (decode_qp_result >= 0) { + data[(*num_decoded)] = decode_qp_result; + (*num_decoded)++; + }; + } + else { + data[(*num_decoded)] = *p; + (*num_decoded)++; + p++; + }; + }; + return data; + /* ----------------------------------------------------------------- */ + } + /* unknown encoding type, just dump as-is */ + else goto NO_DECODING; +} + + +FILE *mime_get_decode_file(uschar *pname, uschar *fname) { + FILE *f; + uschar *filename; + + filename = (uschar *)malloc(2048); + + if ((pname != NULL) && (fname != NULL)) { + snprintf(CS filename, 2048, "%s/%s", pname, fname); + f = fopen(CS filename,"w+"); + } + else if (pname == NULL) { + f = fopen(CS fname,"w+"); + } + else if (fname == NULL) { + int file_nr = 0; + int result = 0; + + /* must find first free sequential filename */ + do { + struct stat mystat; + snprintf(CS filename,2048,"%s/%s-%05u", pname, message_id, file_nr); + file_nr++; + /* security break */ + if (file_nr >= 1024) + break; + result = stat(CS filename,&mystat); + } + while(result != -1); + f = fopen(CS filename,"w+"); + }; + + /* set expansion variable */ + mime_decoded_filename = filename; + + return f; +} + + +int mime_decode(uschar **listptr) { + int sep = 0; + uschar *list = *listptr; + uschar *option; + uschar option_buffer[1024]; + uschar decode_path[1024]; + FILE *decode_file = NULL; + uschar *buffer = NULL; + long f_pos = 0; + unsigned int size_counter = 0; + + if (mime_stream == NULL) + return FAIL; + + f_pos = ftell(mime_stream); + + /* build default decode path (will exist since MBOX must be spooled up) */ + snprintf(CS decode_path,1024,"%s/scan/%s",spool_directory,message_id); + + /* reserve a line buffer to work in */ + buffer = (uschar *)malloc(MIME_MAX_LINE_LENGTH+1); + if (buffer == NULL) { + log_write(0, LOG_PANIC, + "decode ACL condition: can't allocate %d bytes of memory.", MIME_MAX_LINE_LENGTH+1); + return DEFER; + }; + + /* try to find 1st option */ + if ((option = string_nextinlist(&list, &sep, + option_buffer, + sizeof(option_buffer))) != NULL) { + + /* parse 1st option */ + if ( (Ustrcmp(option,"false") == 0) || (Ustrcmp(option,"0") == 0) ) { + /* explicitly no decoding */ + return FAIL; + }; + + if (Ustrcmp(option,"default") == 0) { + /* explicit default path + file names */ + goto DEFAULT_PATH; + }; + + if (option[0] == '/') { + struct stat statbuf; + + memset(&statbuf,0,sizeof(statbuf)); + + /* assume either path or path+file name */ + if ( (stat(CS option, &statbuf) == 0) && S_ISDIR(statbuf.st_mode) ) + /* is directory, use it as decode_path */ + decode_file = mime_get_decode_file(option, NULL); + else + /* does not exist or is a file, use as full file name */ + decode_file = mime_get_decode_file(NULL, option); + } + else + /* assume file name only, use default path */ + decode_file = mime_get_decode_file(decode_path, option); + } + else + /* no option? patch default path */ + DEFAULT_PATH: decode_file = mime_get_decode_file(decode_path, NULL); + + if (decode_file == NULL) + return DEFER; + + /* read data linewise and dump it to the file, + while looking for the current boundary */ + while(fgets(CS buffer, MIME_MAX_LINE_LENGTH, mime_stream) != NULL) { + uschar *decoded_line = NULL; + int decoded_line_length = 0; + + if (mime_current_boundary != NULL) { + /* boundary line must start with 2 dashes */ + if (Ustrncmp(buffer,"--",2) == 0) { + if (Ustrncmp((buffer+2),mime_current_boundary,Ustrlen(mime_current_boundary)) == 0) + break; + }; + }; + + decoded_line = mime_parse_line(buffer, mime_content_transfer_encoding, &decoded_line_length); + /* write line to decode file */ + if (fwrite(decoded_line, 1, decoded_line_length, decode_file) < decoded_line_length) { + /* error/short write */ + clearerr(mime_stream); + fseek(mime_stream,f_pos,SEEK_SET); + return DEFER; + }; + size_counter += decoded_line_length; + + if (size_counter > 1023) { + if ((mime_content_size + (size_counter / 1024)) < 65535) + mime_content_size += (size_counter / 1024); + else + mime_content_size = 65535; + size_counter = (size_counter % 1024); + }; + + free(decoded_line); + } + + fclose(decode_file); + + clearerr(mime_stream); + fseek(mime_stream,f_pos,SEEK_SET); + + /* round up remaining size bytes to one k */ + if (size_counter) { + mime_content_size++; + }; + + return OK; +} + +int mime_get_header(FILE *f, uschar *header) { + int c = EOF; + int done = 0; + int header_value_mode = 0; + int header_open_brackets = 0; + int num_copied = 0; + + while(!done) { + + c = fgetc(f); + if (c == EOF) break; + + /* always skip CRs */ + if (c == '\r') continue; + + if (c == '\n') { + if (num_copied > 0) { + /* look if next char is '\t' or ' ' */ + c = fgetc(f); + if (c == EOF) break; + if ( (c == '\t') || (c == ' ') ) continue; + ungetc(c,f); + }; + /* end of the header, terminate with ';' */ + c = ';'; + done = 1; + }; + + /* skip control characters */ + if (c < 32) continue; + + if (header_value_mode) { + /* --------- value mode ----------- */ + /* skip leading whitespace */ + if ( ((c == '\t') || (c == ' ')) && (header_value_mode == 1) ) + continue; + + /* we have hit a non-whitespace char, start copying value data */ + header_value_mode = 2; + + /* skip quotes */ + if (c == '"') continue; + + /* leave value mode on ';' */ + if (c == ';') { + header_value_mode = 0; + }; + /* -------------------------------- */ + } + else { + /* -------- non-value mode -------- */ + /* skip whitespace + tabs */ + if ( (c == ' ') || (c == '\t') ) + continue; + if (c == '\\') { + /* quote next char. can be used + to escape brackets. */ + c = fgetc(f); + if (c == EOF) break; + } + else if (c == '(') { + header_open_brackets++; + continue; + } + else if ((c == ')') && header_open_brackets) { + header_open_brackets--; + continue; + } + else if ( (c == '=') && !header_open_brackets ) { + /* enter value mode */ + header_value_mode = 1; + }; + + /* skip chars while we are in a comment */ + if (header_open_brackets > 0) + continue; + /* -------------------------------- */ + }; + + /* copy the char to the buffer */ + header[num_copied] = (uschar)c; + /* raise counter */ + num_copied++; + + /* break if header buffer is full */ + if (num_copied > MIME_MAX_HEADER_SIZE-1) { + done = 1; + }; + }; + + if (header[num_copied-1] != ';') { + header[num_copied-1] = ';'; + }; + + /* 0-terminate */ + header[num_copied] = '\0'; + + /* return 0 for EOF or empty line */ + if ((c == EOF) || (num_copied == 1)) + return 0; + else + return 1; +} + + +int mime_acl_check(FILE *f, struct mime_boundary_context *context, uschar + **user_msgptr, uschar **log_msgptr) { + int rc = OK; + uschar *header = NULL; + struct mime_boundary_context nested_context; + + /* reserve a line buffer to work in */ + header = (uschar *)malloc(MIME_MAX_HEADER_SIZE+1); + if (header == NULL) { + log_write(0, LOG_PANIC, + "acl_smtp_mime: can't allocate %d bytes of memory.", MIME_MAX_HEADER_SIZE+1); + return DEFER; + }; + + /* Not actually used at the moment, but will be vital to fixing + * some RFC 2046 nonconformance later... */ + nested_context.parent = context; + + /* loop through parts */ + while(1) { + + /* reset all per-part mime variables */ + mime_anomaly_level = NULL; + mime_anomaly_text = NULL; + mime_boundary = NULL; + mime_charset = NULL; + mime_decoded_filename = NULL; + mime_filename = NULL; + mime_content_description = NULL; + mime_content_disposition = NULL; + mime_content_id = NULL; + mime_content_transfer_encoding = NULL; + mime_content_type = NULL; + mime_is_multipart = 0; + mime_content_size = 0; + + /* + If boundary is null, we assume that *f is positioned on the start of headers (for example, + at the very beginning of a message. + If a boundary is given, we must first advance to it to reach the start of the next header + block. + */ + + /* NOTE -- there's an error here -- RFC2046 specifically says to + * check for outer boundaries. This code doesn't do that, and + * I haven't fixed this. + * + * (I have moved partway towards adding support, however, by adding + * a "parent" field to my new boundary-context structure.) + */ + if (context != NULL) { + while(fgets(CS header, MIME_MAX_HEADER_SIZE, f) != NULL) { + /* boundary line must start with 2 dashes */ + if (Ustrncmp(header,"--",2) == 0) { + if (Ustrncmp((header+2),context->boundary,Ustrlen(context->boundary)) == 0) { + /* found boundary */ + if (Ustrncmp((header+2+Ustrlen(context->boundary)),"--",2) == 0) { + /* END boundary found */ + debug_printf("End boundary found %s\n", context->boundary); + return rc; + } + else { + debug_printf("Next part with boundary %s\n", context->boundary); + }; + /* can't use break here */ + goto DECODE_HEADERS; + } + }; + } + /* Hit EOF or read error. Ugh. */ + debug_printf("Hit EOF ...\n"); + return rc; + }; + + DECODE_HEADERS: + /* parse headers, set up expansion variables */ + while(mime_get_header(f,header)) { + int i; + /* loop through header list */ + for (i = 0; i < mime_header_list_size; i++) { + uschar *header_value = NULL; + int header_value_len = 0; + + /* found an interesting header? */ + if (strncmpic(mime_header_list[i].name,header,mime_header_list[i].namelen) == 0) { + uschar *p = header + mime_header_list[i].namelen; + /* yes, grab the value (normalize to lower case) + and copy to its corresponding expansion variable */ + while(*p != ';') { + *p = tolower(*p); + p++; + }; + header_value_len = (p - (header + mime_header_list[i].namelen)); + header_value = (uschar *)malloc(header_value_len+1); + memset(header_value,0,header_value_len+1); + p = header + mime_header_list[i].namelen; + Ustrncpy(header_value, p, header_value_len); + debug_printf("Found %s MIME header, value is '%s'\n", mime_header_list[i].name, header_value); + *((uschar **)(mime_header_list[i].value)) = header_value; + + /* make p point to the next character after the closing ';' */ + p += (header_value_len+1); + + /* grab all param=value tags on the remaining line, check if they are interesting */ + NEXT_PARAM_SEARCH: while (*p != 0) { + int j; + for (j = 0; j < mime_parameter_list_size; j++) { + uschar *param_value = NULL; + int param_value_len = 0; + + /* found an interesting parameter? */ + if (strncmpic(mime_parameter_list[j].name,p,mime_parameter_list[j].namelen) == 0) { + uschar *q = p + mime_parameter_list[j].namelen; + /* yes, grab the value and copy to its corresponding expansion variable */ + while(*q != ';') q++; + param_value_len = (q - (p + mime_parameter_list[j].namelen)); + param_value = (uschar *)malloc(param_value_len+1); + memset(param_value,0,param_value_len+1); + q = p + mime_parameter_list[j].namelen; + Ustrncpy(param_value, q, param_value_len); + param_value = rfc2047_decode(param_value, TRUE, NULL, 32, ¶m_value_len, &q); + debug_printf("Found %s MIME parameter in %s header, value is '%s'\n", mime_parameter_list[j].name, mime_header_list[i].name, param_value); + *((uschar **)(mime_parameter_list[j].value)) = param_value; + p += (mime_parameter_list[j].namelen + param_value_len + 1); + goto NEXT_PARAM_SEARCH; + }; + } + /* There is something, but not one of our interesting parameters. + Advance to the next semicolon */ + while(*p != ';') p++; + p++; + }; + }; + }; + }; + + /* set additional flag variables (easier access) */ + if ( (mime_content_type != NULL) && + (Ustrncmp(mime_content_type,"multipart",9) == 0) ) + mime_is_multipart = 1; + + /* Make a copy of the boundary pointer. + Required since mime_boundary is global + and can be overwritten further down in recursion */ + nested_context.boundary = mime_boundary; + + /* raise global counter */ + mime_part_count++; + + /* copy current file handle to global variable */ + mime_stream = f; + mime_current_boundary = context ? context->boundary : 0; + + /* Note the context */ + mime_is_coverletter = !(context && context->context == MBC_ATTACHMENT); + + /* call ACL handling function */ + rc = acl_check(ACL_WHERE_MIME, NULL, acl_smtp_mime, user_msgptr, log_msgptr); + + mime_stream = NULL; + mime_current_boundary = NULL; + + if (rc != OK) break; + + /* If we have a multipart entity and a boundary, go recursive */ + if ( (mime_content_type != NULL) && + (nested_context.boundary != NULL) && + (Ustrncmp(mime_content_type,"multipart",9) == 0) ) { + debug_printf("Entering multipart recursion, boundary '%s'\n", nested_context.boundary); + + if (context && context->context == MBC_ATTACHMENT) + nested_context.context = MBC_ATTACHMENT; + else if (!Ustrcmp(mime_content_type,"multipart/alternative") + || !Ustrcmp(mime_content_type,"multipart/related")) + nested_context.context = MBC_COVERLETTER_ALL; + else + nested_context.context = MBC_COVERLETTER_ONESHOT; + + rc = mime_acl_check(f, &nested_context, user_msgptr, log_msgptr); + if (rc != OK) break; + } + else if ( (mime_content_type != NULL) && + (Ustrncmp(mime_content_type,"message/rfc822",14) == 0) ) { + uschar *rfc822name = NULL; + uschar filename[2048]; + int file_nr = 0; + int result = 0; + + /* must find first free sequential filename */ + do { + struct stat mystat; + snprintf(CS filename,2048,"%s/scan/%s/__rfc822_%05u", spool_directory, message_id, file_nr); + file_nr++; + /* security break */ + if (file_nr >= 128) + goto NO_RFC822; + result = stat(CS filename,&mystat); + } + while(result != -1); + + rfc822name = filename; + + /* decode RFC822 attachment */ + mime_decoded_filename = NULL; + mime_stream = f; + mime_current_boundary = context ? context->boundary : NULL; + mime_decode(&rfc822name); + mime_stream = NULL; + mime_current_boundary = NULL; + if (mime_decoded_filename == NULL) { + /* decoding failed */ + log_write(0, LOG_MAIN, + "mime_regex acl condition warning - could not decode RFC822 MIME part to file."); + return DEFER; + }; + mime_decoded_filename = NULL; + }; + + NO_RFC822: + /* If the boundary of this instance is NULL, we are finished here */ + if (context == NULL) break; + + if (context->context == MBC_COVERLETTER_ONESHOT) + context->context = MBC_ATTACHMENT; + + }; + + return rc; +} + + diff --git a/src/src/mime.h b/src/src/mime.h new file mode 100644 index 000000000..f9d67c349 --- /dev/null +++ b/src/src/mime.h @@ -0,0 +1,79 @@ +/* $Cambridge: exim/src/src/mime.h,v 1.1.2.1 2004/11/26 09:13:34 tom Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* This file is part of the exiscan-acl content scanner +patch. It is NOT part of the standard exim distribution. */ + +/* Copyright (c) Tom Kistner 2004 */ +/* License: GPL */ + + +#define MIME_MAX_HEADER_SIZE 8192 +#define MIME_MAX_LINE_LENGTH 32768 + +#define MBC_ATTACHMENT 0 +#define MBC_COVERLETTER_ONESHOT 1 +#define MBC_COVERLETTER_ALL 2 + +struct mime_boundary_context +{ + struct mime_boundary_context *parent; + unsigned char *boundary; + int context; +}; + +typedef struct mime_header { + uschar *name; + int namelen; + void *value; +} mime_header; + +static mime_header mime_header_list[] = { + { US"content-type:", 13, &mime_content_type }, + { US"content-disposition:", 20, &mime_content_disposition }, + { US"content-transfer-encoding:", 26, &mime_content_transfer_encoding }, + { US"content-id:", 11, &mime_content_id }, + { US"content-description:", 20 , &mime_content_description } +}; + +static int mime_header_list_size = sizeof(mime_header_list)/sizeof(mime_header); + + + +typedef struct mime_parameter { + uschar *name; + int namelen; + void *value; +} mime_parameter; + +static mime_parameter mime_parameter_list[] = { + { US"name=", 5, &mime_filename }, + { US"filename=", 9, &mime_filename }, + { US"charset=", 8, &mime_charset }, + { US"boundary=", 9, &mime_boundary } +}; + +static int mime_parameter_list_size = sizeof(mime_parameter_list)/sizeof(mime_parameter); + +/* BASE64 decoder matrix */ +static unsigned char mime_b64[256]={ +/* 0 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +/* 16 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +/* 32 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 62, 128, 128, 128, 63, +/* 48 */ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 128, 128, 128, 255, 128, 128, +/* 64 */ 128, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, +/* 80 */ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 128, 128, 128, 128, 128, +/* 96 */ 128, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, +/* 112 */ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 128, 128, 128, 128, 128, +/* 128 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +/* 144 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +/* 160 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +/* 176 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +/* 192 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +/* 208 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +/* 224 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +/* 240 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128 +}; diff --git a/src/src/spam.c b/src/src/spam.c new file mode 100644 index 000000000..877243502 --- /dev/null +++ b/src/src/spam.c @@ -0,0 +1,340 @@ +/* $Cambridge: exim/src/src/spam.c,v 1.1.2.1 2004/11/26 09:13:34 tom Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* This file is part of the exiscan-acl content scanner +patch. It is NOT part of the standard exim distribution. */ + +/* Copyright (c) Tom Kistner 2003-???? */ +/* License: GPL */ + +/* Code for calling spamassassin's spamd. Called from acl.c. */ + +#include "exim.h" +#include "spam.h" + +uschar spam_score_buffer[16]; +uschar spam_score_int_buffer[16]; +uschar spam_bar_buffer[128]; +uschar spam_report_buffer[32600]; +uschar prev_user_name[128] = ""; +int spam_ok = 0; +int spam_rc = 0; + +int spam(uschar **listptr) { + int sep = 0; + uschar *list = *listptr; + uschar *user_name; + uschar user_name_buffer[128]; + unsigned long long mbox_size; + FILE *mbox_file; + int spamd_sock; + uschar spamd_buffer[32600]; + int i, j, offset; + uschar spamd_version[8]; + uschar spamd_score_char; + double spamd_threshold, spamd_score; + int spamd_report_offset; + uschar *p,*q; + int override = 0; + struct sockaddr_un server; + + /* find the username from the option list */ + if ((user_name = string_nextinlist(&list, &sep, + user_name_buffer, + sizeof(user_name_buffer))) == NULL) { + /* no username given, this means no scanning should be done */ + return FAIL; + }; + + /* if username is "0" or "false", do not scan */ + if ( (Ustrcmp(user_name,"0") == 0) || + (strcmpic(user_name,US"false") == 0) ) { + return FAIL; + }; + + /* if there is an additional option, check if it is "true" */ + if (strcmpic(list,US"true") == 0) { + /* in that case, always return true later */ + override = 1; + }; + + /* if we scanned for this username last time, just return */ + if ( spam_ok && ( Ustrcmp(prev_user_name, user_name) == 0 ) ) { + if (override) + return OK; + else + return spam_rc; + }; + + /* make sure the eml mbox file is spooled up */ + mbox_file = spool_mbox(&mbox_size); + + if (mbox_file == NULL) { + /* error while spooling */ + log_write(0, LOG_MAIN|LOG_PANIC, + "spam acl condition: error while creating mbox spool file"); + return DEFER; + }; + + /* socket does not start with '/' -> network socket */ + if (*spamd_address != '/') { + time_t now = time(NULL); + int num_servers = 0; + int current_server = 0; + int start_server = 0; + uschar *address = NULL; + uschar *spamd_address_list_ptr = spamd_address; + uschar address_buffer[256]; + spamd_address_container * spamd_address_vector[32]; + + /* Check how many spamd servers we have + and register their addresses */ + while ((address = string_nextinlist(&spamd_address_list_ptr, &sep, + address_buffer, + sizeof(address_buffer))) != NULL) { + + spamd_address_container *this_spamd = + (spamd_address_container *)store_get(sizeof(spamd_address_container)); + + /* grok spamd address and port */ + if( sscanf(CS address, "%s %u", this_spamd->tcp_addr, &(this_spamd->tcp_port)) != 2 ) { + log_write(0, LOG_MAIN, + "spam acl condition: warning - invalid spamd address: '%s'", address); + continue; + }; + + spamd_address_vector[num_servers] = this_spamd; + num_servers++; + if (num_servers > 31) + break; + }; + + /* check if we have at least one server */ + if (!num_servers) { + log_write(0, LOG_MAIN|LOG_PANIC, + "spam acl condition: no useable spamd server addresses in spamd_address configuration option."); + fclose(mbox_file); + return DEFER; + }; + + current_server = start_server = (int)now % num_servers; + + while (1) { + + debug_printf("trying server %s, port %u\n", + spamd_address_vector[current_server]->tcp_addr, + spamd_address_vector[current_server]->tcp_port); + + /* contact a spamd */ + if ( (spamd_sock = ip_socket(SOCK_STREAM, AF_INET)) < 0) { + log_write(0, LOG_MAIN|LOG_PANIC, + "spam acl condition: error creating IP socket for spamd"); + fclose(mbox_file); + return DEFER; + }; + + if (ip_connect( spamd_sock, + AF_INET, + spamd_address_vector[current_server]->tcp_addr, + spamd_address_vector[current_server]->tcp_port, + 5 ) > -1) { + /* connection OK */ + break; + }; + + log_write(0, LOG_MAIN|LOG_PANIC, + "spam acl condition: warning - spamd connection to %s, port %u failed: %s", + spamd_address_vector[current_server]->tcp_addr, + spamd_address_vector[current_server]->tcp_port, + strerror(errno)); + current_server++; + if (current_server >= num_servers) + current_server = 0; + if (current_server == start_server) { + log_write(0, LOG_MAIN|LOG_PANIC, "spam acl condition: all spamd servers failed"); + fclose(mbox_file); + close(spamd_sock); + return DEFER; + }; + }; + + } + else { + /* open the local socket */ + + if ((spamd_sock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: spamd: unable to acquire socket (%s)", + strerror(errno)); + fclose(mbox_file); + return DEFER; + } + + server.sun_family = AF_UNIX; + Ustrcpy(server.sun_path, spamd_address); + + if (connect(spamd_sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) { + log_write(0, LOG_MAIN|LOG_PANIC, + "malware acl condition: spamd: unable to connect to UNIX socket %s (%s)", + spamd_address, strerror(errno) ); + fclose(mbox_file); + close(spamd_sock); + return DEFER; + } + + } + + /* now we are connected to spamd on spamd_sock */ + snprintf(CS spamd_buffer, + sizeof(spamd_buffer), + "REPORT SPAMC/1.2\r\nUser: %s\r\nContent-length: %lld\r\n\r\n", + user_name, + mbox_size); + + /* send our request */ + if (send(spamd_sock, spamd_buffer, Ustrlen(spamd_buffer), 0) < 0) { + close(spamd_sock); + log_write(0, LOG_MAIN|LOG_PANIC, + "spam acl condition: spamd send failed: %s", strerror(errno)); + fclose(mbox_file); + close(spamd_sock); + return DEFER; + }; + + /* now send the file */ + do { + j = fread(spamd_buffer,1,sizeof(spamd_buffer),mbox_file); + if (j > 0) { + i = send(spamd_sock,spamd_buffer,j,0); + if (i != j) { + log_write(0, LOG_MAIN|LOG_PANIC, + "spam acl condition: error/short send to spamd"); + close(spamd_sock); + fclose(mbox_file); + return DEFER; + }; + }; + } + while (j > 0); + + fclose(mbox_file); + + /* we're done sending, close socket for writing */ + shutdown(spamd_sock,SHUT_WR); + + /* read spamd response */ + memset(spamd_buffer, 0, sizeof(spamd_buffer)); + offset = 0; + while((i = ip_recv(spamd_sock, + spamd_buffer + offset, + sizeof(spamd_buffer) - offset - 1, + SPAMD_READ_TIMEOUT)) > 0 ) { + offset += i; + } + + /* error handling */ + if((i <= 0) && (errno != 0)) { + log_write(0, LOG_MAIN|LOG_PANIC, + "spam acl condition: error reading from spamd socket: %s", strerror(errno)); + close(spamd_sock); + return DEFER; + } + + /* reading done */ + close(spamd_sock); + + /* dig in the spamd output and put the report in a multiline header, if requested */ + if( sscanf(CS spamd_buffer,"SPAMD/%s 0 EX_OK\r\nContent-length: %*u\r\n\r\n%lf/%lf\r\n%n", + spamd_version,&spamd_score,&spamd_threshold,&spamd_report_offset) != 3 ) { + + /* try to fall back to pre-2.50 spamd output */ + if( sscanf(CS spamd_buffer,"SPAMD/%s 0 EX_OK\r\nSpam: %*s ; %lf / %lf\r\n\r\n%n", + spamd_version,&spamd_score,&spamd_threshold,&spamd_report_offset) != 3 ) { + log_write(0, LOG_MAIN|LOG_PANIC, + "spam acl condition: cannot parse spamd output"); + return DEFER; + }; + }; + + /* Create report. Since this is a multiline string, + we must hack it into shape first */ + p = &spamd_buffer[spamd_report_offset]; + q = spam_report_buffer; + while (*p != '\0') { + /* skip \r */ + if (*p == '\r') { + p++; + continue; + }; + *q = *p; + q++; + if (*p == '\n') { + *q = '\t'; + q++; + /* eat whitespace */ + while( (*p <= ' ') && (*p != '\0') ) { + p++; + }; + p--; + }; + p++; + }; + /* NULL-terminate */ + *q = '\0'; + q--; + /* cut off trailing leftovers */ + while (*q <= ' ') { + *q = '\0'; + q--; + }; + spam_report = spam_report_buffer; + + /* create spam bar */ + spamd_score_char = spamd_score > 0 ? '+' : '-'; + j = abs((int)(spamd_score)); + i = 0; + if( j != 0 ) { + while((i < j) && (i <= MAX_SPAM_BAR_CHARS)) + spam_bar_buffer[i++] = spamd_score_char; + } + else{ + spam_bar_buffer[0] = '/'; + i = 1; + } + spam_bar_buffer[i] = '\0'; + spam_bar = spam_bar_buffer; + + /* create "float" spam score */ + snprintf(CS spam_score_buffer, sizeof(spam_score_buffer),"%.1f", spamd_score); + spam_score = spam_score_buffer; + + /* create "int" spam score */ + j = (int)((spamd_score + 0.001)*10); + snprintf(CS spam_score_int_buffer, sizeof(spam_score_int_buffer), "%d", j); + spam_score_int = spam_score_int_buffer; + + /* compare threshold against score */ + if (spamd_score >= spamd_threshold) { + /* spam as determined by user's threshold */ + spam_rc = OK; + } + else { + /* not spam */ + spam_rc = FAIL; + }; + + /* remember user name and "been here" for it */ + Ustrcpy(prev_user_name, user_name); + spam_ok = 1; + + if (override) { + /* always return OK, no matter what the score */ + return OK; + } + else { + return spam_rc; + }; +} diff --git a/src/src/spam.h b/src/src/spam.h new file mode 100644 index 000000000..4d72a4cf2 --- /dev/null +++ b/src/src/spam.h @@ -0,0 +1,32 @@ +/* $Cambridge: exim/src/src/spam.h,v 1.1.2.1 2004/11/26 09:13:34 tom Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* This file is part of the exiscan-acl content scanner +patch. It is NOT part of the standard exim distribution. */ + +/* Copyright (c) Tom Kistner 2003-???? */ +/* License: GPL */ + +/* spam defines */ + +/* timeout for reading from spamd */ +#define SPAMD_READ_TIMEOUT 3600 + +/* maximum length of the spam bar */ +#define MAX_SPAM_BAR_CHARS 50 + +/* SHUT_WR seems to be undefined on Unixware ? */ +#ifndef SHUT_WR +#define SHUT_WR 1 +#endif + +typedef struct spamd_address_container { + uschar tcp_addr[24]; + unsigned int tcp_port; +} spamd_address_container; + + + diff --git a/src/src/spool_mbox.c b/src/src/spool_mbox.c new file mode 100644 index 000000000..946a07287 --- /dev/null +++ b/src/src/spool_mbox.c @@ -0,0 +1,198 @@ +/* $Cambridge: exim/src/src/spool_mbox.c,v 1.1.2.1 2004/11/26 09:13:34 tom Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* This file is part of the exiscan-acl content scanner +patch. It is NOT part of the standard exim distribution. */ + +/* Copyright (c) Tom Kistner 2003-???? */ +/* License: GPL */ + +/* Code for setting up a MBOX style spool file inside a /scan/ +sub directory of exim's spool directory. */ + +#include "exim.h" + +/* externals, we must reset them on unspooling */ +extern int demime_ok; +extern int malware_ok; +extern int spam_ok; +extern struct file_extension *file_extensions; + +int spool_mbox_ok = 0; +uschar spooled_message_id[17]; + +/* returns a pointer to the FILE, and puts the size in bytes into mbox_file_size */ + +FILE *spool_mbox(unsigned long long *mbox_file_size) { + uschar mbox_path[1024]; + uschar message_subdir[2]; + uschar data_buffer[65535]; + FILE *mbox_file; + FILE *data_file = NULL; + header_line *my_headerlist; + struct stat statbuf; + int i,j; + + /* + uschar *received; + uschar *timestamp; + */ + + if (!spool_mbox_ok) { + /* create scan directory, if not present */ + if (!directory_make(spool_directory, US "scan", 0750, FALSE)) { + debug_printf("unable to create directory: %s/scan\n", spool_directory); + return NULL; + }; + + /* create temp directory inside scan dir */ + snprintf(CS mbox_path, 1024, "%s/scan/%s", spool_directory, message_id); + if (!directory_make(NULL, mbox_path, 0750, FALSE)) { + debug_printf("unable to create directory: %s/scan/%s\n", spool_directory, message_id); + return NULL; + }; + + /* open [message_id].eml file for writing */ + snprintf(CS mbox_path, 1024, "%s/scan/%s/%s.eml", spool_directory, message_id, message_id); + mbox_file = Ufopen(mbox_path,"w"); + + if (mbox_file == NULL) { + debug_printf("unable to open file for writing: %s\n", mbox_path); + return NULL; + }; + + /* Generate a preliminary Received: header and put it in the file. + We need to do this so SA can do DNS list checks */ + + /* removed for 4.34 + + timestamp = expand_string(US"${tod_full}"); + received = expand_string(received_header_text); + if (received != NULL) { + uschar *my_received; + if (received[0] == 0) { + my_received = string_sprintf("Received: ; %s\n", timestamp); + } + else { + my_received = string_sprintf("%s; %s\n", received, timestamp); + } + i = fwrite(my_received, 1, Ustrlen(my_received), mbox_file); + if (i != Ustrlen(my_received)) { + debug_printf("error/short write on writing in: %s", mbox_path); + fclose(mbox_file); + return NULL; + }; + }; + + */ + + /* write all header lines to mbox file */ + my_headerlist = header_list; + while (my_headerlist != NULL) { + + /* skip deleted headers */ + if (my_headerlist->type == '*') { + my_headerlist = my_headerlist->next; + continue; + }; + + i = fwrite(my_headerlist->text, 1, my_headerlist->slen, mbox_file); + if (i != my_headerlist->slen) { + debug_printf("error/short write on writing in: %s", mbox_path); + fclose(mbox_file); + return NULL; + }; + + my_headerlist = my_headerlist->next; + }; + + /* copy body file */ + message_subdir[1] = '\0'; + for (i = 0; i < 2; i++) { + message_subdir[0] = (split_spool_directory == (i == 0))? message_id[5] : 0; + sprintf(CS mbox_path, "%s/input/%s/%s-D", spool_directory, message_subdir, message_id); + data_file = Ufopen(mbox_path,"r"); + if (data_file != NULL) + break; + }; + + fread(data_buffer, 1, 18, data_file); + + do { + j = fread(data_buffer, 1, sizeof(data_buffer), data_file); + if (j > 0) { + i = fwrite(data_buffer, 1, j, mbox_file); + if (i != j) { + debug_printf("error/short write on writing in: %s", mbox_path); + fclose(mbox_file); + fclose(data_file); + return NULL; + }; + }; + } while (j > 0); + + fclose(data_file); + fclose(mbox_file); + Ustrcpy(spooled_message_id, message_id); + spool_mbox_ok = 1; + }; + + snprintf(CS mbox_path, 1024, "%s/scan/%s/%s.eml", spool_directory, message_id, message_id); + + /* get the size of the mbox message */ + stat(CS mbox_path, &statbuf); + *mbox_file_size = statbuf.st_size; + + /* open [message_id].eml file for reading */ + mbox_file = Ufopen(mbox_path,"r"); + + return mbox_file; +} + +/* remove mbox spool file, demimed files and temp directory */ +void unspool_mbox(void) { + + /* reset all exiscan state variables */ + demime_ok = 0; + demime_errorlevel = 0; + demime_reason = NULL; + file_extensions = NULL; + spam_ok = 0; + malware_ok = 0; + + if (spool_mbox_ok) { + + spool_mbox_ok = 0; + + if (!no_mbox_unspool) { + uschar mbox_path[1024]; + uschar file_path[1024]; + int n; + struct dirent *entry; + DIR *tempdir; + + snprintf(CS mbox_path, 1024, "%s/scan/%s", spool_directory, spooled_message_id); + + tempdir = opendir(CS mbox_path); + /* loop thru dir & delete entries */ + n = 0; + do { + entry = readdir(tempdir); + if (entry == NULL) break; + snprintf(CS file_path, 1024,"%s/scan/%s/%s", spool_directory, spooled_message_id, entry->d_name); + if ( (Ustrcmp(entry->d_name,"..") != 0) && (Ustrcmp(entry->d_name,".") != 0) ) { + debug_printf("unspool_mbox(): unlinking '%s'\n", file_path); + n = unlink(CS file_path); + }; + } while (n > -1); + + closedir(tempdir); + + /* remove directory */ + n = rmdir(CS mbox_path); + }; + }; +}