Mercurial > notdcc
view dccifd/dccifd.c @ 0:c7f6b056b673
First import of vendor version
author | Peter Gervai <grin@grin.hu> |
---|---|
date | Tue, 10 Mar 2009 13:49:58 +0100 |
parents | |
children |
line wrap: on
line source
/* Distributed Checksum Clearinghouse * * DCC interface daemon * * Copyright (c) 2008 by Rhyolite Software, LLC * * This agreement is not applicable to any entity which sells anti-spam * solutions to others or provides an anti-spam solution as part of a * security solution sold to other entities, or to a private network * which employs the DCC or uses data provided by operation of the DCC * but does not provide corresponding data to other users. * * Permission to use, copy, modify, and distribute this software without * changes for any purpose with or without fee is hereby granted, provided * that the above copyright notice and this permission notice appear in all * copies and any distributed versions or copies are either unchanged * or not called anything similar to "DCC" or "Distributed Checksum * Clearinghouse". * * Parties not eligible to receive a license under this agreement can * obtain a commercial license to use DCC by contacting Rhyolite Software * at sales@rhyolite.com. * * A commercial license would be for Distributed Checksum and Reputation * Clearinghouse software. That software includes additional features. This * free license for Distributed ChecksumClearinghouse Software does not in any * way grant permision to use Distributed Checksum and Reputation Clearinghouse * software * * THE SOFTWARE IS PROVIDED "AS IS" AND RHYOLITE SOFTWARE, LLC DISCLAIMS ALL * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL RHYOLITE SOFTWARE, LLC * BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES * OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, * WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS * SOFTWARE. * * Rhyolite Software DCC 1.3.103-1.166 $Revision$ */ #include "dccif.h" #include "dcc_paths.h" #include "cmn_defs.h" #include <signal.h> static int stopping; #define MAX_SMTP_LINE u_char use_ipv6 = 0; /* incoming proxy and bidirectional ASCII dccifd protocol connection */ static char *listen_addr; static u_char listen_family; static struct sockaddr_un listen_sun; static const char *lhost_port; static const char *rcidr; static struct in6_addr raddr, rmask; static SOCKET listen_soc = -1; /* outgoing proxy connection */ static u_char proxy_out_family; static struct sockaddr_un proxy_out_sun; static char proxy_out_host[DCC_MAXDOMAINLEN]; static u_int16_t proxy_out_port; static DCC_SOCKU proxy_out_su; static const char *rundir = DCC_RUNDIR; static DCC_PATH pidpath; static const char *progpath = DCC_LIBEXECDIR"/dccifd"; static u_char background = 1; static u_char proxy; u_char cannot_discard = 0; /* can trim targets after DATA */ u_char cannot_reject = 1; /* ASCII protocol accepts all targets */ /* message state or context */ typedef struct { /* control an input buffer */ char *base; char *in; char *out; char *next_line; int size; int line_len; int *socp; } IN_BC; typedef struct { /* control an ouput buffer */ char *base; char *in; int len; int size; int *socp; } OUT_BC; typedef struct work { int proxy_in_soc; DCC_SOCKU proxy_in_su; int proxy_out_soc; char buf1[DCC_HDR_CK_MAX*8]; /* >=DCC_HDR_CK_MAX*2 & MAX_RCPTS */ char buf2[DCC_HDR_CK_MAX*8]; char buf3[1024]; char buf4[1024]; CMN_WORK cw; enum { SMTP_ST_START, /* expecting HELO */ SMTP_ST_HELO, /* seen HELO, expecting Mail_From */ SMTP_ST_TRANS, /* seen Mail_From */ SMTP_ST_RCPT, /* seen Rcpt_To */ SMTP_ST_ERROR /* no transaction until RSET or HELO */ } smtp_state; /* from here down is zeroed when the structure is allocated */ #define WORK_ZERO fwd struct work *fwd; IN_BC msg_rd; /* incoming mail message */ OUT_BC msg_wt; /* outoing mail message */ IN_BC reply_in; /* incoming SMTP replies */ OUT_BC reply_out; /* outgoing SMTP replies */ int total_hdrs, cr_hdrs; int parse_rcvd; /* which received: header to parse */ u_int dfgs; # define DFG_WORK_LOCK 0x0001 /* hold the lock */ # define DFG_MISSING_BODY 0x0002 /* missing message body */ # define DFG_MTA_BODY 0x0004 /* MTA wants the body */ # define DFG_MTA_HEADER 0x0008 /* MTA wants the X-DCC header */ # define DFG_SEEN_HDR 0x0010 /* have at least 1 header */ # define DFG_PARSE_RCVD 0x0020 /* parse Received header */ # define DFG_XCLIENT_NAME 0x0040 /* client name via XCLIENT */ # define DFG_XCLIENT_ADDR 0x0080 /* client addresses via XCLIENT */ # define DFG_XCLIENT_HELO 0x0100 /* HELO value via XCLIENT */ # define DFG_RECYCLE (DFG_WORK_LOCK | DFG_MTA_BODY \ | DFG_XCLIENT_NAME | DFG_XCLIENT_ADDR \ | DFG_XCLIENT_HELO) } WORK; /* use a free list to avoid malloc() overhead */ static WORK *work_free; /* each dccifd job involves * a socket connected to the MTA or upstream proxy * a socket connected to the downstream proxy if using SMTP * a log file, * and a socket to talk to the DCC server. * The file descriptors for the whitelists are accounted for in EXTRA_FILES */ #define FILES_PER_JOB 4 int max_max_work = MAX_SELECT_WORK; typedef struct user_domain { struct user_domain *fwd; const char *nm; int len; u_char wildcard; } USER_DOMAIN; static USER_DOMAIN *user_domains; static char hostname[MAXHOSTNAMELEN+1] = "@"; /* max seconds to wait for MTA * Longer than the longest timeout in RFC 2821 */ #define MAX_MTA_DELAY (10*60+5) static void sigterm(int); static u_char set_soc(DCC_EMSG, int, int, const char *); static void bind_listen(void); static void close_listen_soc(void); static void unlink_listen_sun(void); static void NRATTRIB *job_start(void *); static void job_close(WORK *); static void NRATTRIB job_exit(WORK *); static void NRATTRIB proxy_msg_truncated(WORK *); static void add_work(int); static void usage(const char* barg, const char *bvar) { const char str[] = { "usage: [-VdbxANQ] [-G on | off | noIP | IPmask/xx] [-h homedir]" " [-I user]\n" " [-p /sock | host,port,rhost/bits]" " [-o /sock | host,port]\n" " [-D local-domain] [-r rejection-msg] [-m map] [-w whiteclnt]\n" " [-U userdirs] [-a IGNORE | REJECT | DISCARD]\n" " [-t type,[log-thold,][rej-thold]] [-g [not-]type]" " [-S header]\n" " [-l logdir] [-R rundir] [-T tmpdir] [-j maxjobs]\n" " [-B dnsbl-option] [-L ltype,facility.level]\n" }; static u_char complained; if (!complained) { if (barg) dcc_error_msg("unrecognized \"%s%s\"\nusage: %s\n..." " continuing", barg, bvar, str); else dcc_error_msg("%s\n... continuing", str); complained = 1; } } int NRATTRIB main(int argc, char **argv) { DCC_EMSG emsg; #ifdef RLIMIT_NOFILE struct rlimit nofile; int old_rlim_cur; #endif long l; u_char log_tgts_set = 0; const char *homedir = 0; const char *logdir = 0; const char *tmpdir = 0; WORK *wp; pthread_t tid; DCC_SOCKLEN_T namelen; const char *cp; USER_DOMAIN *udom, *udom2, **udomp; char *p; int error, i; emsg[0] = '\0'; if (*argv[0] == '/') progpath = argv[0]; dcc_syslog_init(1, argv[0], 0); dcc_clear_tholds(); #ifdef RLIMIT_NOFILE if (0 > getrlimit(RLIMIT_NOFILE, &nofile)) { dcc_error_msg("getrlimit(RLIMIT_NOFILE): %s", ERROR_STR()); old_rlim_cur = 1000*1000; } else { old_rlim_cur = nofile.rlim_cur; if (nofile.rlim_max < 1000*1000) { i = nofile.rlim_max; #ifndef USE_POLL if (i > FD_SETSIZE) i = FD_SETSIZE; #endif max_max_work = (i - EXTRA_FILES)/FILES_PER_JOB; max_max_work_src = "RLIMIT_NOFILE limit"; } } #endif /* RLIMIT_NOFILE */ if (max_max_work <= 0) { dcc_error_msg("too few open files allowed"); max_max_work = MIN_MAX_WORK; } max_work = max_max_work; #define SLARGS "64VdbxANQW" /* fix start-dccifd if these change */ while (-1 != (i = getopt(argc, argv, SLARGS"G:h:I:p:o:D:r:m:w:U:" "a:t:g:S:l:R:T:j:B:L:"))) { switch (i) { #ifndef NO_IPV6 case '6': use_ipv6 = 1; break; #endif case '4': use_ipv6 = 0; break; case 'V': fprintf(stderr, DCC_VERSION"\n"); exit(EX_OK); break; case 'd': ++dcc_clnt_debug; break; case 'b': background = 0; break; case 'x': try_extra_hard = DCC_CLNT_FG_NO_FAIL; break; case 'A': chghdr = ADDHDR; break; case 'N': chghdr = NOHDR; break; case 'Q': dcc_query_only = 1; break; case 'G': if (!dcc_parse_client_grey(optarg)) usage("-G", optarg); break; case 'W': /* obsolete DCC off by default */ to_white_only = 1; break; case 'h': homedir = optarg; break; case 'I': dcc_daemon_su(optarg); break; case 'p': listen_addr = optarg; break; case 'o': proxy = 1; cannot_reject = 0; cannot_discard = 1; p = strchr(optarg, ','); if (!p) { /* recognize single-ended (not-really-)-proxy */ if (!strcmp(optarg, _PATH_DEVNULL)) { proxy_out_family = AF_UNSPEC; break; } if (strlen(optarg)>=ISZ(proxy_out_sun.sun_path)) dcc_logbad(EX_USAGE, "invalid UNIX" " domain socket: -o %s", optarg); strcpy(proxy_out_sun.sun_path, optarg); #ifdef HAVE_SA_LEN proxy_out_sun.sun_len = SUN_LEN(&proxy_out_sun); #endif proxy_out_sun.sun_family = AF_UNIX; proxy_out_family = AF_UNIX; } else { cp = dcc_parse_nm_port(emsg, optarg, DCC_GET_PORT_INVALID, proxy_out_host, sizeof(proxy_out_host), &proxy_out_port, 0, 0, 0, 0); if (!cp) dcc_logbad(dcc_ex_code, "%s", emsg); if (*cp != '\0') dcc_logbad(EX_USAGE, "invalid IP address: \"%s\"", optarg); proxy_out_family = AF_INET; /* includes IPv6 */ } break; case 'D': /* save user domain names sorted by length * so that we can apply the most restrictive */ i = strlen(optarg); if (*optarg == '\0' || ((optarg[0] == '@' || optarg[0] == '*') && i < 2) || strpbrk(optarg+1, "@*")) { dcc_logbad(EX_USAGE, "invalid local-domain \"%s\"", optarg); break; } udom2 = dcc_malloc(sizeof(*udom2)); memset(udom2, 0, sizeof(*udom2)); udom2->len = i; udom2->nm = optarg; if (optarg[0] == '*') { udom2->wildcard = 1; ++udom2->nm; --udom2->len; } udomp = &user_domains; for (;;) { udom = *udomp; /* insert longer names before shorter names * insert wildcard before same, non-wildcard * because i includes the '*' */ if (!udom || i > udom->len) { udom2->fwd = udom; *udomp = udom2; break; } udomp = &udom->fwd; } break; case 'r': parse_reply_arg(optarg); break; case 'm': mapfile_nm = optarg; break; case 'w': main_white_nm = optarg; break; case 'U': parse_userdirs(optarg); break; case 'a': if (!strcasecmp(optarg, "IGNORE")) { action = CMN_IGNORE; } else if (!strcasecmp(optarg, "REJECT")) { action = CMN_REJECT; } else if (!strcasecmp(optarg, "DISCARD")) { action = CMN_DISCARD; } else { dcc_error_msg("unrecognized -a action: %s", optarg); } break; case 't': if (dcc_parse_tholds("-t ", optarg)) log_tgts_set = 1; break; case 'g': /* honor not-spam "counts" */ dcc_parse_honor(optarg); break; case 'S': dcc_add_sub_hdr(0, optarg); break; case 'l': /* log rejected mail here */ logdir = optarg; break; case 'R': rundir = optarg; break; case 'T': tmpdir = optarg; break; case 'j': /* maximum simultaneous jobs */ l = strtoul(optarg, &p, 10); if (*p != '\0' || l < MIN_MAX_WORK) { dcc_error_msg("invalid queue length %s", optarg); } else if (l > max_max_work) { dcc_error_msg("-j queue length %s" " larger than %s; using %d", optarg, max_max_work_src, max_max_work); max_work = max_max_work; } else { max_work = l; } break; case 'B': if (!dcc_parse_dnsbl(emsg, optarg, progpath, 0)) dcc_error_msg("%s", emsg); break; case 'L': if (dcc_parse_log_opt(optarg)) helper_save_arg("-L", optarg); break; default: usage(optopt2str(optopt), ""); } } argc -= optind; argv += optind; if (argc != 0) usage(argv[0], ""); /* default -D setting to the local host name */ if (!user_domains) { if (0 > gethostname(hostname+1, sizeof(hostname)-2)) { dcc_error_msg("gethostname(): %s", ERROR_STR()); } else if ((i = strlen(hostname)) > 1) { user_domains = dcc_malloc(sizeof(*user_domains)); memset(user_domains, 0, sizeof(*user_domains)); user_domains->len = i; user_domains->nm = hostname; } } dcc_cdhome(0, homedir, 0); dcc_main_logdir_init(0, logdir); tmp_path_init(tmpdir, logdir); if (proxy_out_family == AF_INET) { if (proxy_out_host[0] == '\0' || !strcmp(proxy_out_host, "@")) { /* null or "@" means incoming host */ ; } else { dcc_host_lock(); if (!dcc_get_host(proxy_out_host, use_ipv6, &error)) dcc_logbad(EX_NOHOST, "%s: %s", proxy_out_host, DCC_HSTRERROR(error)); proxy_out_su = dcc_hostaddrs[0]; *DCC_SU_PORTP(&proxy_out_su) = proxy_out_port; dcc_host_unlock(); } } /* Open the incoming socket before our backgrounding fork() to * minimize races and allow better error reporting. */ bind_listen(); if (dcc_main_logdir[0] == '\0') { if (log_tgts_set) dcc_error_msg("log thresholds set with -t" " but no -l directory"); if (userdirs != '\0') dcc_error_msg("no -l directory prevents per-user" " logging with -U"); } #ifdef RLIMIT_NOFILE if (old_rlim_cur < (i = max_work*FILES_PER_JOB+EXTRA_FILES)) { nofile.rlim_cur = i; if (0 > setrlimit(RLIMIT_NOFILE, &nofile)) { dcc_error_msg("setrlimit(RLIMIT_NOFILE,%d): %s", i, ERROR_STR()); max_work = old_rlim_cur/FILES_PER_JOB - EXTRA_FILES; if (max_work <= 0) { dcc_error_msg("only %d open files allowed" " by RLIMIT_NOFILE", old_rlim_cur); max_work = MIN_MAX_WORK; } } } #endif /* RLIMIT_NOFILE */ helper_init(max_work); if (background) { if (daemon(1, 0) < 0) dcc_logbad(EX_OSERR, "daemon(): %s", ERROR_STR()); dcc_daemon_restart(rundir, 0); dcc_pidfile(pidpath, rundir); } signal(SIGPIPE, SIG_IGN); signal(SIGHUP, sigterm); signal(SIGTERM, sigterm); signal(SIGINT, sigterm); #ifdef SIGXFSZ signal(SIGXFSZ, SIG_IGN); #endif /* Be careful to start all threads only after the fork() in daemon(), * because some POSIX threads packages (e.g. FreeBSD) get confused * about threads in the parent. */ cmn_init(); add_work(init_work); if (listen_family != AF_UNIX) { dcc_trace_msg(DCC_VERSION" listening to %s from %s for %s", lhost_port, rcidr, proxy ? "SMTP commands" : "ASCII protocol"); } else { dcc_trace_msg(DCC_VERSION" listening to %s for %s", listen_addr, proxy ? "SMTP commands" : "ASCII protocol"); } if (dcc_clnt_debug) dcc_trace_msg("init_work=%d max_work=%d max_max_work=%d (%s)", total_work, max_work, max_max_work, max_max_work_src); while (!stopping) { /* delay for 1 second instead of forever to notice * when SIGTERM has said to stop */ i = dcc_select_poll(emsg, listen_soc, 1, DCC_US); if (i < 0) dcc_logbad(EX_OSERR, "%s", emsg); if (i == 0) continue; /* A new connection is ready. Allocate a context * block and create a thread */ lock_work(); wp = work_free; if (!wp) { if (total_work > max_work) { /* pretend we weren't listening if we * are out of context blocks */ unlock_work(); sleep(1); continue; } if (dcc_clnt_debug > 1) dcc_trace_msg("add %d work blocks to %d", init_work, total_work); add_work(init_work); wp = work_free; } work_free = wp->fwd; unlock_work(); /* clear most of context block for the new connection */ cmn_clear(&wp->cw, wp, 1); wp->cw.helo[0] = '\0'; memset(&wp->WORK_ZERO, 0, sizeof(*wp) - ((char*)&wp->WORK_ZERO - (char*)wp)); namelen = sizeof(wp->proxy_in_su); wp->proxy_in_soc = accept(listen_soc, &wp->proxy_in_su.sa, &namelen); if (wp->proxy_in_soc < 0) { dcc_error_msg("accept(): %s", ERROR_STR()); job_close(wp); continue; } if (listen_family != AF_UNIX) { struct in6_addr addr6, *ap; if (wp->proxy_in_su.sa.sa_family == AF_INET6) { ap = &wp->proxy_in_su.ipv6.sin6_addr; } else { ap = &addr6; dcc_ipv4toipv6(ap, wp->proxy_in_su.ipv4.sin_addr); } if (!DCC_IN_BLOCK(*ap, raddr, rmask)) { char str[DCC_SU2STR_SIZE]; dcc_error_msg("unauthorized client address %s", dcc_su2str(str, sizeof(str), &wp->proxy_in_su)); job_close(wp); continue; } } if (!set_soc(emsg, wp->proxy_in_soc, listen_family, "MTA")) { dcc_error_msg("%s", emsg); job_close(wp); continue; } i = pthread_create(&tid, 0, job_start, wp); if (i) { dcc_error_msg("pthread_create(): %s", ERROR_STR1(i)); job_close(wp); continue; } i = pthread_detach(tid); if (i != 0) { if (i != ESRCH) dcc_error_msg("pthread_detach(): %s", ERROR_STR1(i)); else if (dcc_clnt_debug) dcc_trace_msg("pthread_detach(): %s", ERROR_STR1(i)); } } close_listen_soc(); totals_stop(); exit(stopping); } static void unlink_listen_sun(void) { if (listen_family != AF_UNIX) return; /* there is an unavoidable race here */ if (0 > unlink(listen_sun.sun_path) && errno != ENOENT) dcc_error_msg("unlink(%s): %s", listen_sun.sun_path, ERROR_STR()); } static void bind_unix_listen(void) { DCC_EMSG emsg; struct stat sb; int i; #ifdef HP_UX_BAD_AF_UNIX dcc_logbad(EX_CONFIG, "HP-UX AF_UNIX does not support shutdown()"); #endif emsg[0] = '\0'; listen_family = AF_UNIX; if (!listen_addr) { /* use default UNIX domain socket */ snprintf(listen_sun.sun_path, sizeof(listen_sun.sun_path), "%s/"DCC_DCCIF_UDS, dcc_homedir); listen_addr = listen_sun.sun_path; } else { if (strlen(listen_addr) >= ISZ(listen_sun.sun_path)) dcc_logbad(EX_USAGE, "invalid UNIX domain socket: %s", listen_addr); strcpy(listen_sun.sun_path, listen_addr); } #ifdef HAVE_SA_LEN listen_sun.sun_len = SUN_LEN(&listen_sun); #endif listen_sun.sun_family = AF_UNIX; if (0 <= stat(listen_sun.sun_path, &sb) && !(S_ISSOCK(sb.st_mode) || S_ISFIFO(sb.st_mode))) dcc_logbad(EX_UNAVAILABLE, "non-socket present at %s", listen_sun.sun_path); /* Look for a daemon already using our socket. Do not give up * immediately in case a previous instance is slowly stopping. */ i = 0; for (;;) { listen_soc = socket(AF_UNIX, SOCK_STREAM, 0); if (listen_soc < 0) dcc_logbad(EX_OSERR, "socket(AF_UNIX): %s", ERROR_STR()); /* unlink it only if it looks like a dead socket */ if (0 > connect(listen_soc, (struct sockaddr *)&listen_sun, sizeof(listen_sun))) { if (errno == ECONNREFUSED || errno == ECONNRESET || errno == EACCES) { unlink_listen_sun(); } else if (dcc_clnt_debug > 2 && errno != ENOENT) { dcc_trace_msg("connect(old server %s): %s", listen_sun.sun_path, ERROR_STR()); } close(listen_soc); break; } /* connect() worked so the socket is alive */ if (++i > 5*10) dcc_logbad(EX_UNAVAILABLE, "something running with socket at %s", listen_sun.sun_path); close(listen_soc); usleep(100*1000); } listen_soc = socket(AF_UNIX, SOCK_STREAM, 0); if (listen_soc < 0) dcc_logbad(EX_OSERR, "socket(AF_UNIX): %s", ERROR_STR()); if (0 > bind(listen_soc, (struct sockaddr *)&listen_sun, sizeof(listen_sun))) dcc_logbad(EX_IOERR, "bind(%s) %s", listen_sun.sun_path, ERROR_STR()); if (0 > chmod(listen_sun.sun_path, 0666)) dcc_error_msg("chmod(%s, 0666): %s", listen_sun.sun_path, ERROR_STR()); } static void bind_tcp_listen(void) { DCC_EMSG emsg; char lhost[MAXHOSTNAMELEN]; u_int16_t lport; DCC_SOCKU su; char *duparg; const char *cp; int on, error; emsg[0] = '\0'; duparg = strdup(listen_addr); lhost_port = duparg; duparg = strchr(duparg, ','); if (!duparg) dcc_logbad(EX_USAGE, "missing port number in \"%s\"", listen_addr); duparg = strchr(duparg+1, ','); if (!duparg) dcc_logbad(EX_USAGE, "missing rhost in \"%s\"", listen_addr); *duparg++ = '\0'; rcidr = duparg; if (0 >= dcc_str2cidr(emsg, &raddr, &rmask, 0, rcidr, 0, 0)) dcc_logbad(EX_USAGE, "invalid rhost and mask \"%s\"", rcidr); cp = dcc_parse_nm_port(emsg, lhost_port, DCC_GET_PORT_INVALID, lhost, sizeof(lhost), &lport, 0, 0, 0, 0); if (!cp) dcc_logbad(dcc_ex_code, "%s", emsg); if (*cp != '\0') dcc_logbad(EX_USAGE, "invalid IP address: \"%s\"", lhost_port); if (lhost[0] == '\0' || !strcmp(lhost, "@")) { /* null or "@" means INADDR_ANY */ dcc_mk_su(&su, use_ipv6 ? AF_INET6: AF_INET, 0, lport); } else { dcc_host_lock(); if (!dcc_get_host(lhost, use_ipv6, &error)) dcc_logbad(EX_NOHOST, "%s: %s", lhost, DCC_HSTRERROR(error)); su = dcc_hostaddrs[0]; *DCC_SU_PORTP(&su) = lport; dcc_host_unlock(); } listen_soc = socket(su.sa.sa_family, SOCK_STREAM, 0); if (listen_soc < 0) dcc_logbad(EX_OSERR, "socket(): %s", ERROR_STR()); on = 1; if (0 > setsockopt(listen_soc, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) dcc_error_msg("setsockopt(listen %s, SO_REUSADDR): %s", dcc_su2str_err(&su), ERROR_STR()); if (0 > bind(listen_soc, &su.sa, DCC_SU_LEN(&su))) dcc_logbad(EX_UNAVAILABLE, "bind(%s) %s", lhost_port, ERROR_STR()); listen_family = su.sa.sa_family; } static u_char set_soc(DCC_EMSG emsg, int s, int family, const char *sname) { int on; if (0 > fcntl(s, F_SETFD, FD_CLOEXEC)) { dcc_pemsg(EX_IOERR, emsg, "fcntl(%s, F_SETFD, FD_CLOEXEC): %s", sname, ERROR_STR()); return 0; } if (family != AF_UNIX) { on = 1; if (0 > setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on))) { dcc_pemsg(EX_IOERR, emsg, "setsockopt(%s, SO_KEEPALIVE): %s", sname, ERROR_STR()); return 0; } } /* use non-blocking sockets so that we can read entire lines * without knowing how big they are or expecting the operating * system to allow peeking at its buffers */ if (-1 == fcntl(s, F_SETFL, fcntl(s, F_GETFL, 0) | O_NONBLOCK)) { dcc_pemsg(EX_OSERR, emsg, "fcntl(%s, O_NONBLOCK): %s", sname, ERROR_STR()); return 0; } return 1; } static void bind_listen(void) { DCC_EMSG emsg; char *p; /* It is a TCP address if it has a port number and mask. * Otherwise it is a UNIX domain socket. */ if (listen_addr != 0 && (p = strchr(listen_addr, ',')) != 0 && strchr(p, ',')) { bind_tcp_listen(); } else { bind_unix_listen(); } if (!set_soc(emsg, listen_soc, listen_family, "main socket")) dcc_logbad(dcc_ex_code, "%s", emsg); if (0 > listen(listen_soc, 10)) dcc_logbad(EX_IOERR, "listen(): %s", ERROR_STR()); } static u_char /* 0=EOF, 1=read something */ soc_read(WORK *wp, IN_BC *bc) { int fspace, len, total, i; if (bc->out >= bc->in) bc->out = bc->in = bc->base; fspace = &bc->base[bc->size] - bc->in; if (fspace < bc->size/8) { if (bc->out == bc->base) { thr_error_msg(&wp->cw, "buffer overrun; in=%d", (int)(bc->in - bc->base)); job_exit(wp); } len = bc->in - bc->out; memmove(bc->base, bc->out, len); bc->out = bc->base; bc->in = bc->out+len; fspace = bc->size - len; } if (wp->dfgs & DFG_WORK_LOCK) unlock_work(); if (!bc->socp || *bc->socp < 0) dcc_logbad(EX_SOFTWARE, "attempt to read closed socket"); for (;;) { i = dcc_select_poll(wp->cw.emsg, *bc->socp, 1, MAX_MTA_DELAY*DCC_US); if (i > 0) break; if (i < 0) { thr_error_msg(&wp->cw, "%s", wp->cw.emsg); } else { thr_error_msg(&wp->cw, "MTA read timeout"); } job_exit(wp); } total = read(*bc->socp, bc->in, fspace); if (total < 0) { if (!proxy || dcc_clnt_debug) thr_error_msg(&wp->cw, "read(sock): %s", ERROR_STR()); job_exit(wp); } bc->in += total; if (wp->dfgs & DFG_WORK_LOCK) lock_work(); return total != 0; } /* ensure there is another line in the buffer */ static u_char /* 0=eof or buffer overflow */ msg_read_line(WORK *wp, IN_BC *bc) { int i; char *p; for (;;) { p = bc->out; i = bc->in - p; if (i != 0) { /* look for <LF> for ASCII protocol * or <CR><LF> for SMTP */ p = memchr(p, '\n', i); if (p && (!proxy || (p > bc->out && *(p-1) == '\r'))) { bc->next_line = p+1; return 1; } } if (!soc_read(wp, bc)) return 0; } } static void buf_write_flush(WORK *wp, OUT_BC *bc) { const char *buf; int len, i; len = bc->len; if (!len) return; bc->len = 0; if (!bc->socp || *bc->socp < 0) dcc_logbad(EX_SOFTWARE, "attempt to write closed socket"); buf = bc->base; do { i = dcc_select_poll(wp->cw.emsg, *bc->socp, 0, MAX_MTA_DELAY*DCC_US); if (i < 0) { thr_error_msg(&wp->cw, "%s", wp->cw.emsg); job_exit(wp); } if (i == 0) { thr_error_msg(&wp->cw, "MTA write timeout"); job_exit(wp); } i = write(*bc->socp, buf, len); if (i < 0) { if (DCC_BLOCK_ERROR()) continue; thr_error_msg(&wp->cw, "write(MTA socket,%d): %s", len, ERROR_STR()); job_exit(wp); } if (i == 0) { thr_error_msg(&wp->cw, "write(MTA socket,%d)=%d", len, i); job_exit(wp); } buf += i; len -= i; } while (len > 0); } static void buf_write(WORK *wp, OUT_BC *bc, const void *buf, u_int len) { u_int n; for (;;) { n = bc->size - bc->len; if (n == 0) dcc_logbad(EX_SOFTWARE, "impossible buffer space"); if (n > len) n = len; memcpy(&bc->base[bc->len], buf, n); if ((bc->len += n) >= bc->size) buf_write_flush(wp, bc); if ((len -= n) == 0) return; buf = (void *)((char *)buf + n); } } static void tmp_write(WORK *wp, const void *buf, int len) { if (!cmn_write_tmp(&wp->cw, buf, len)) { thr_error_msg(&wp->cw, "%s", wp->cw.emsg); job_exit(wp); } } static int tmp_read(WORK *wp, void *buf, int len) { int i; if (wp->cw.tmp_fd < 0) return 0; i = read(wp->cw.tmp_fd, buf, len); if (i < 0) { thr_error_msg(&wp->cw, "read(%s,%d): %s", wp->cw.tmp_nm, len, ERROR_STR()); job_exit(wp); } return i; } /* fill MTA read buffer from temporary file */ static int tmp_read_msg_in(WORK *wp) { int i; /* preserving what is already in it */ i = wp->msg_rd.in - wp->msg_rd.out; if (i > 0) memmove(wp->msg_rd.base, wp->msg_rd.out, i); wp->msg_rd.out = wp->msg_rd.base; wp->msg_rd.in = wp->msg_rd.out+i; wp->msg_rd.in += tmp_read(wp, wp->msg_rd.in, wp->msg_rd.size - i); return wp->msg_rd.in > wp->msg_rd.out; } /* Create the contexts. */ static void add_work(int i) { WORK *wp; total_work += i; wp = dcc_malloc(sizeof(*wp)*i); memset(wp, 0, sizeof(*wp)*i); while (i-- != 0) { wp->proxy_in_soc = -1; wp->proxy_out_soc = -1; cmn_create(&wp->cw); wp->fwd = work_free; work_free = wp; ++wp; } } void work_clean(void) { WORK *wp; int keep, delete; lock_work(); keep = 5; delete = init_work; for (wp = work_free; wp; wp = wp->fwd) { if (!wp->cw.dcc_ctxt) break; if (--keep > 0) continue; dcc_clnt_soc_close(wp->cw.dcc_ctxt); if (--delete <= 0) break; } unlock_work(); } static void job_close(WORK *wp) { if (wp->dfgs & DFG_WORK_LOCK) { wp->dfgs &= ~DFG_WORK_LOCK; unlock_work(); } wp->msg_rd.socp = 0; if (wp->msg_wt.socp) { buf_write_flush(wp, &wp->msg_wt); wp->msg_wt.socp = 0; } wp->reply_in.socp = 0; if (wp->reply_out.socp) { buf_write_flush(wp, &wp->reply_out); wp->reply_out.socp = 0; } cmn_close_tmp(&wp->cw); if (wp->proxy_in_soc >= 0) { if (0 > close(wp->proxy_in_soc) && (dcc_clnt_debug || (errno != ECONNRESET && errno != ENOTCONN))) thr_error_msg(&wp->cw, "close(proxy input socket): %s", ERROR_STR()); wp->proxy_in_soc = -1; } if (wp->proxy_out_soc >= 0) { if (0 > close(wp->proxy_out_soc)) thr_error_msg(&wp->cw, "close(proxy output socket): %s", ERROR_STR()); wp->proxy_out_soc = -1; } log_stop(&wp->cw); lock_work(); free_rcpt_sts(&wp->cw, 0); wp->fwd = work_free; work_free = wp; unlock_work(); } static void NRATTRIB job_exit(WORK *wp) { job_close(wp); pthread_exit(0); /* mostly to suppress warning */ dcc_logbad(EX_OSERR, "pthread_exit() returned"); } /* write headers or body data from the MTA read buffer to the MTA */ static void mta_write(WORK *wp, char *end) { int len; len = end - wp->msg_rd.out; if (len <= 0) return; buf_write(wp, &wp->msg_wt, wp->msg_rd.out, len); wp->msg_rd.out = end; } /* copy headers from the temporary file to the MTA */ static void hdrs_copy(WORK *wp) { enum {START_HDR, SKIP_HDR, COPY_HDR} cmode; const char *nl; char *bol, *eol; int i; if (-1 == lseek(wp->cw.tmp_fd, 0, SEEK_SET)) { thr_error_msg(&wp->cw, "rewind %s: %s", wp->cw.tmp_nm, ERROR_STR()); job_exit(wp); } cmode = START_HDR; wp->msg_rd.in = wp->msg_rd.out = wp->msg_rd.base; for (;;) { /* fill wp->msg_rd while keeping anything already present */ if (!tmp_read_msg_in(wp)) return; bol = wp->msg_rd.out; while ((i = wp->msg_rd.in - bol) > 0) { /* Find the end of the next line. */ eol = memchr(bol, '\n', i); if (!eol) { /* Fill the buffer if we can't find '\n'' * and the buffer has room */ if (i < wp->msg_rd.size && wp->msg_rd.out != wp->msg_rd.base) break; /* pretend the header ended with the buffer * if there is no room */ eol = wp->msg_rd.in-1; nl = 0; } else if (eol+1 >= wp->msg_rd.in) { /* get the character after '\n' */ if (i < wp->msg_rd.size && wp->msg_rd.out != wp->msg_rd.base) break; /* pretend we could not find it if there * is no room in the buffer * or if we are at end of file. * This will also end the headers */ nl = 0; } else { nl = eol+1; } if (cmode == START_HDR) { /* We are at start of a header or the body. * Quit before line of "\n" or "\r\n" */ if (eol == bol || (eol == bol+1 && *bol == '\r')) { /* write any preceding lines */ mta_write(wp, bol); return; } /* Look for our header * Assume the buffer is larger than the * largest possible X-DCC field name. */ if (chghdr == SETHDR && is_xhdr(bol, eol-bol)) { /* skip it * and copy any preceding headers. */ mta_write(wp, bol); cmode = SKIP_HDR; } else { cmode = COPY_HDR; } } if (cmode == SKIP_HDR) wp->msg_rd.out = eol+1; /* Check the character after '\n' for * whitespace indicating continuation */ if (nl && *nl != ' ' && *nl != '\t') { cmode = START_HDR; /* deal with SMTP transparency */ if (proxy && *nl == '.') *eol-- = '.'; } bol = eol+1; } mta_write(wp, bol); } } static void add_hdr(void *wp0, const char *buf, u_int buf_len) { WORK *wp = wp0; buf_write(wp, &wp->msg_wt, buf, buf_len); } static void body_copy(WORK *wp) { u_char seen_crlf; char *p; hdrs_copy(wp); if (chghdr != NOHDR && wp->cw.header.buf[0] != '\0') { /* write X-DCC header * end with "\r\n" if at least * half of the header lines ended that way */ xhdr_write(add_hdr, wp, wp->cw.header.buf, wp->cw.header.used, wp->cr_hdrs > wp->total_hdrs/2); } /* copy body */ seen_crlf = 2; do { p = wp->msg_rd.out; while (p < wp->msg_rd.in) { if (seen_crlf == 1) { seen_crlf = (*p++ == '\n') ? 2 : 0; continue; } if (seen_crlf == 2) { seen_crlf = 0; if (*p == '.' && proxy) { mta_write(wp, p); buf_write(wp, &wp->msg_wt, ".", 1); } } if (!proxy) break; p = memchr(p, '\r', wp->msg_rd.in-p); if (!p) break; seen_crlf = 1; ++p; } mta_write(wp, wp->msg_rd.in); } while (tmp_read_msg_in(wp)); } static void close_listen_soc(void) { if (pidpath[0] != '\0') { unlink(pidpath); pidpath[0] = '\0'; } if (listen_soc >= 0) { unlink_listen_sun(); if (0 > close(listen_soc)) dcc_error_msg("close(main socket): %s", ERROR_STR()); listen_soc = -1; } } /* watch for fatal signals */ static void sigterm(int sig) { stopping = 100 + sig; unlink_listen_sun(); dcc_clnt_stop_resolve(); signal(sig, SIG_DFL); /* quit on repeated signals */ } void user_reject_discard(UATTRIB CMN_WORK *cwp, UATTRIB RCPT_ST *rcpt_st) { /* dccifd has no way to tell the MTA to remove a recipient * after the Rcpt_To command */ thr_error_msg(cwp, "cannot discard an individual target"); } static void get_helo(WORK *wp, const char *vp, int len) { if (len < DCC_HELO_MAX-1) { memcpy(wp->cw.helo, vp, len); wp->cw.helo[len] = '\0'; } else { /* if the HELO value is too long, capture what we can * and indicate that we got only part of it */ len = DCC_HELO_MAX-1; memcpy(wp->cw.helo, vp, len); strcpy(&wp->msg_rd.out[DCC_HELO_MAX-ISZ(DCC_HELO_CONT)], DCC_HELO_CONT); } } /* Get the next header field * The line is copied to the temporary file and then null terminated * in the buffer */ static u_char /* 1=have one, 0=end of headers */ get_hdr(WORK *wp) { int tf_len; /* header bytes written to temp file */ int hdr_len; /* total header length */ int nlen; /* length of next chunk of header */ char *np; /* next byte after field */ char *p; u_char at_bol; /* 1=at start of a line of header */ tf_len = 0; hdr_len = 0; at_bol = 1; for (;;) { /* get another line of the header */ np = wp->msg_rd.out + hdr_len; nlen = wp->msg_rd.in - np; if (nlen <= 1) { /* we do not have enough data */ read_more:; if (hdr_len > DCC_HDR_CK_MAX) { /* we already have more than DCC_HDR_CK_MAX * bytes of the header. * Keep only DCC_HDR_CK_MAX bytes of it * in the buffer. We will eventually * return only the first DCC_HDR_CK_MAX * bytes and an arbitrary part of the tail * to our caller. */ if (tf_len < hdr_len) tmp_write(wp, wp->msg_rd.out+tf_len, hdr_len - tf_len); /* discard all but the first DCC_HDR_CK_MAX * bytes of the header */ hdr_len = DCC_HDR_CK_MAX; tf_len = DCC_HDR_CK_MAX; if (nlen != 0) wp->msg_rd.out[DCC_HDR_CK_MAX] = *np; wp->msg_rd.in = (wp->msg_rd.out + DCC_HDR_CK_MAX + nlen); } /* Get more data, possibly sliding what we already * have down in the buffer. That would move * wp->msg_rd.in to wp->msg_rd.base. */ if (!soc_read(wp, &wp->msg_rd)) { /* EOF implies the message body including the * separating blank line is missing. * That is impossible for the ASCII dccifd * protocol because there should be at least * a Received header. * When running as a proxy, we should get * "\r\n.\r\n" * Fake newlines until we get out of here * so that we will log whatever we got */ wp->dfgs |= DFG_MISSING_BODY; if (wp->cr_hdrs >= wp->total_hdrs/2) *wp->msg_rd.in++ = '\r'; *wp->msg_rd.in++ = '\n'; } continue; } if (at_bol) { /* deal with SMTP transparency */ if (*np == '.' && proxy) { if (nlen < 3) goto read_more; if (np[1] != '\r' || np[2] != '\n') { memmove(np, np+1, --nlen); } else if (hdr_len == 0) { /* ".\r\n" at the start of a line * ends the message. In this case * the message body including the * separating blank line is missing. * Stop short before the ".\r\n" */ return 0; } } /* stop before the next line if it is * not a continuation of the current header */ if (hdr_len != 0 && *np != ' ' && *np != '\t') { if (tf_len < hdr_len) tmp_write(wp, wp->msg_rd.out+tf_len, hdr_len - tf_len); wp->msg_rd.out[hdr_len-1] = '\0'; if (hdr_len > 1 && wp->msg_rd.out[hdr_len-2] == '\r') ++wp->cr_hdrs; wp->msg_rd.next_line = &wp->msg_rd.out[hdr_len]; return 1; } } /* find the end of the next line */ p = memchr(np, '\n', nlen); if (p) { hdr_len += ++p - np; /* quit at the end of headers */ if (hdr_len == 1 || (hdr_len == 2 && *wp->msg_rd.out == '\r')) return 0; at_bol = 1; } else { hdr_len += nlen; at_bol = 0; } } } static void get_hdrs(WORK *wp) { const char *p, *rh; for (;;) { /* stop at the separator between the body and headers */ if (!get_hdr(wp)) break; ++wp->total_hdrs; #define GET_HDR_CK(h,t) { \ if (!CLITCMP(wp->msg_rd.out, h)) { \ dcc_get_cks(&wp->cw.cks, DCC_CK_##t, \ &wp->msg_rd.out[LITZ(h)], 1); \ wp->dfgs |= DFG_SEEN_HDR; \ wp->msg_rd.out = wp->msg_rd.next_line; \ continue;}} GET_HDR_CK(DCC_XHDR_TYPE_FROM":", FROM); GET_HDR_CK(DCC_XHDR_TYPE_MESSAGE_ID":", MESSAGE_ID); #undef GET_HDR_CK /* notice UNIX From_ line */ if (!(wp->dfgs & DFG_SEEN_HDR) && wp->cw.env_from[0] == '\0' && parse_unix_from(wp->msg_rd.out, wp->cw.env_from, sizeof(wp->cw.env_from))) { wp->dfgs |= DFG_SEEN_HDR; wp->msg_rd.out = wp->msg_rd.next_line; continue; } if (wp->cw.env_from[0] == '\0' && parse_return_path(wp->msg_rd.out, wp->cw.env_from, sizeof(wp->cw.env_from))) { wp->dfgs |= DFG_SEEN_HDR; wp->msg_rd.out = wp->msg_rd.next_line; continue; } if (!CLITCMP(wp->msg_rd.out, DCC_XHDR_TYPE_RECEIVED":")) { p = &wp->msg_rd.out[LITZ(DCC_XHDR_TYPE_RECEIVED":")]; /* compute checksum of the last Received: header */ dcc_get_cks(&wp->cw.cks, DCC_CK_RECEIVED, p, 1); wp->dfgs |= DFG_SEEN_HDR; wp->msg_rd.out = wp->msg_rd.next_line; /* pick IP address out of first Received: header */ if (!(wp->dfgs & DFG_PARSE_RCVD) || --wp->parse_rcvd >= 0) continue; rh = parse_received(p, &wp->cw.cks, (wp->cw.helo[0] == '\0') ? wp->cw.helo : 0, sizeof(wp->cw.helo), wp->cw.sender_str, sizeof(wp->cw.sender_str), wp->cw.sender_name, sizeof(wp->cw.sender_name)); if (rh == 0) { /* to avoid being fooled by forged * headers, stop at a strange one */ wp->dfgs &= ~DFG_PARSE_RCVD; } else if (*rh != '\0') { thr_log_print(&wp->cw, 1, "skip %s Received: header\n", rh); } else if (!check_mx_listing(&wp->cw)) { /* we know the client */ wp->dfgs &= ~DFG_PARSE_RCVD; } continue; } /* Notice MIME multipart boundary definitions */ dcc_ck_mime_hdr(&wp->cw.cks, wp->msg_rd.out, 0); if (dcc_ck_get_sub(&wp->cw.cks, wp->msg_rd.out, 0)) wp->dfgs |= DFG_SEEN_HDR; /* notice any sort of header */ if (!(wp->dfgs & DFG_SEEN_HDR)) { for (p = wp->msg_rd.out; ; ++p) { if (*p == ':') { wp->dfgs |= DFG_SEEN_HDR; break; } if (*p <= ' ' || *p >= 0x7f) break; } } wp->msg_rd.out = wp->msg_rd.next_line; } /* Create a checksum for a null Message-ID header if there * was no Message-ID header. */ if (wp->cw.cks.sums[DCC_CK_MESSAGE_ID].type != DCC_CK_MESSAGE_ID) dcc_get_cks(&wp->cw.cks, DCC_CK_MESSAGE_ID, "", 0); } /* Assume for now that the sender is the SMTP client. Received: headers * might challenge that assumption */ static void get_sender(WORK *wp) { strcpy(wp->cw.sender_name, wp->cw.clnt_name); strcpy(wp->cw.sender_str, wp->cw.clnt_str); if (wp->cw.sender_str[0] == '\0' || check_mx_listing(&wp->cw)) { /* try Received: header if client is unknown or MX server*/ wp->dfgs |= DFG_PARSE_RCVD; } } static u_char /* 0=temporary failure, 1=ok */ get_body(WORK *wp) { char *p; char buf[1024]; u_char bol; int buflen, i; /* We must make a copy of the entire message in a temporary file * if the MTA wants a copy with the X-DCC header added * Copy the headers to a temporary file because the official * log file needs the SMTP client IP address and envelope information * before the header lines. The log file needs all of the header * lines including stray X-DCC lines, but those lines must be removed * from the output file. */ if (!cmn_open_tmp(&wp->cw)) { if (wp->dfgs & DFG_MTA_BODY) { dcc_error_msg("fatal error: %s", wp->cw.emsg); return 0; } dcc_error_msg("%s", wp->cw.emsg); } get_hdrs(wp); /* log IP address and so forth that we may have collected from * the headers or Postfix XFORWARD or XCLIENT ESTMP extension */ thr_log_envelope(&wp->cw, 0); /* Check DNS blacklists for STMP client and envelope sender * unless DNSBL checks are turned off for all of the recipients */ if (wp->cw.cks.dnsbl) { if (wp->cw.cks.sums[DCC_CK_IP].type == DCC_CK_IP) dcc_client_dnsbl(wp->cw.cks.dnsbl, &wp->cw.cks.ip_addr, wp->cw.sender_name); if (wp->cw.mail_host[0] != '\0') dcc_mail_host_dnsbl(wp->cw.cks.dnsbl, wp->cw.mail_host); } /* copy headers from temporary file to log file */ if (wp->cw.log_fd >= 0 && wp->cw.tmp_fd >= 0) { if (0 > lseek(wp->cw.tmp_fd, 0, SEEK_SET)) { thr_error_msg(&wp->cw, "rewind %s: %s", wp->cw.tmp_nm, ERROR_STR()); job_exit(wp); } for (;;) { buflen = tmp_read(wp, buf, sizeof(buf)); if (buflen <= 0) break; log_body_write(&wp->cw, buf, buflen); } } if (wp->dfgs & DFG_MISSING_BODY) thr_error_msg(&wp->cw, "missing message body"); /* collect the body */ bol = 1; for (;;) { buflen = wp->msg_rd.in - wp->msg_rd.out; if (buflen <= 0) { if (!soc_read(wp, &wp->msg_rd)) break; buflen = wp->msg_rd.in - wp->msg_rd.out; } /* deal with SMTP transparency and detect end of message */ if (proxy) { if (bol) { /* We are at the beginning of a line. * There will always be at least 3 more bytes * in the message, ".\r\n" */ if (buflen < 3) { if (!soc_read(wp, &wp->msg_rd)) proxy_msg_truncated(wp); buflen = wp->msg_rd.in - wp->msg_rd.out; } /* Whether a '.' is the end of the data * or an escaped '.', we will discard it. */ if (*wp->msg_rd.out == '.' && buflen >= 1) { ++wp->msg_rd.out; --buflen; /* if it is followed by "\r\n", then * we have the end of the message */ if (buflen >= 2 && wp->msg_rd.out[0] == '\r' && wp->msg_rd.out[1] == '\n') { wp->msg_rd.out += 2; break; } } /* we are still at the beginning of a line */ } /* check all of the lines in the buffer */ p = wp->msg_rd.out; for (;;) { i = p - wp->msg_rd.out; p = memchr(p, '\r', buflen-i); if (!p) { /* We have checked all of the buffer. * It does not end near an end of line. * So log the block of body lines * and leave the part of a line * ending the buffer for later. */ bol = 0; break; } /* We have found '\r' * Maybe it will be followed by '\n' */ i = ++p - wp->msg_rd.out; if (i+2 >= buflen) { /* '\r' ends or almost ends buffer. */ bol = 0; buflen = i-1; if (buflen > 0) { /* The buffer ends with "\rX" * and contains text before '\r' * Process up to the '\r' */ break; } /* buffer starts with "\r" */ if (!soc_read(wp, &wp->msg_rd)) proxy_msg_truncated(wp); p = wp->msg_rd.out; buflen = wp->msg_rd.in - p; } else if (*p == '\n') { /* We have "\r\n" * If "\r\n" is followed by '.', * then process up to the '.' */ if (*++p == '.') { buflen = i+1; bol = 1; break; } } } } /* Log the body block */ log_body_write(&wp->cw, wp->msg_rd.out, buflen); if (wp->dfgs & DFG_MTA_BODY) tmp_write(wp, wp->msg_rd.out, buflen); dcc_ck_body(&wp->cw.cks, wp->msg_rd.out, buflen); wp->msg_rd.out += buflen; } dcc_cks_fin(&wp->cw.cks); LOG_CAPTION(wp, DCC_LOG_MSG_SEP); thr_log_late(&wp->cw); /* check the grey and white lists */ cmn_ask_white(&wp->cw); /* report spam to the DCC server in the case without a recipient * for the ASCII protocol, * which normally causes a query instead of a report. */ if (!wp->cw.rcpt_st_first && ((wp->cw.ask_st & ASK_ST_MTA_ISSPAM) || (wp->cw.init_sws & FLTR_SW_TRAPS) || dcc_query_only || wp->cw.ask_st & ASK_ST_QUERY)) ++wp->cw.tgts; wp->cw.header.buf[0] = '\0'; wp->cw.header.used = 0; if (wp->cw.tgts > wp->cw.white_tgts) { /* Report to the DCC and add our header if allowed. * After serious errors, act as if DCC server said not-spam * but remove our X-DCC header */ i = cmn_ask_dcc(&wp->cw); if (!i && try_extra_hard) return 0; } else { /* The message is whitelisted or the MTA told us there are 0 * recipients, so we cannot ask the DCC server. * If it was whitelisted, add X-DCC header saying so. */ if (wp->cw.tgts > 0) xhdr_whitelist(&wp->cw.header); /* Use the local target count to decide whether to log * the mail message */ dcc_honor_log_cnts(&wp->cw.ask_st, &wp->cw.cks, wp->cw.tgts); } totals.tgts += wp->cw.tgts; return 1; } /* ensure there is another line, trim its terminal "\r\n", * and add a terminal '\0' * This cannot be used with the proxy code because it often needs to * send the original buffer downstream. */ static int /* # of bytes available */ ascii_read_line(WORK *wp) { char *p; if (!msg_read_line(wp, &wp->msg_rd)) { thr_error_msg(&wp->cw, "truncated request"); job_exit(wp); } p = wp->msg_rd.next_line-1; for (;;) { *p-- = '\0'; if (p < wp->msg_rd.out) return 0; if (*p != '\r') return p+1 - wp->msg_rd.out; } } /* Look for a string from the MTA * while ignoring case and skipping whitespace * The next line must already be in the buffer */ static u_char /* 1=matched it */ ascii_opt_str(WORK *wp, const char *st, int stlen) { /* skip initial white space */ while (wp->msg_rd.out < wp->msg_rd.next_line && (*wp->msg_rd.out == '\t' || *wp->msg_rd.out == ' ')) ++wp->msg_rd.out; if (stlen <= wp->msg_rd.next_line - wp->msg_rd.out && !strncasecmp(wp->msg_rd.out, st, stlen)) { if (wp->msg_rd.out[stlen] == '\0') { wp->msg_rd.out += stlen; return 1; } /* skip trailing whitespace */ if (wp->msg_rd.out[stlen] == '\t' || wp->msg_rd.out[stlen] == ' ') { do { ++stlen; } while (wp->msg_rd.out[stlen] == '\t' || wp->msg_rd.out[stlen] == ' '); wp->msg_rd.out += stlen; return 1; } } return 0; } static u_char /* 0=bad recipient */ check_addr(WORK *wp, const char **addrp, int *addr_lenp, const char **userp, int *user_lenp) { USER_DOMAIN *udom; const char *atchr, *addr, *cp; int addr_len, i; addr = *addrp; addr_len = *addr_lenp; if (addr_len > RCTP_MAXNAME-1) addr_len = *addr_lenp = RCTP_MAXNAME-1; if (*addr == '<' && addr_len >= 2 && addr[addr_len-1] == '>') { ++addr; addr_len -= 2; if (addr_len == 0) { if (wp->dfgs & DFG_WORK_LOCK) unlock_work(); thr_error_msg(&wp->cw, "null recipient address"); if (wp->dfgs & DFG_WORK_LOCK) lock_work(); return 0; } } /* strip source route from recipient */ while (addr_len != 0 && (cp = memchr(addr, ',', addr_len)) != 0) { ++cp; addr_len -= cp-addr; addr = cp; *addr_lenp = addr_len; *addrp = addr; } if (addr_len >= RCTP_MAXNAME) { if (wp->dfgs & DFG_WORK_LOCK) unlock_work(); thr_error_msg(&wp->cw, "recipient \"%s\" is too long", addr); if (wp->dfgs & DFG_WORK_LOCK) lock_work(); return 0; } if (addr_len <= 0) { if (wp->dfgs & DFG_WORK_LOCK) unlock_work(); thr_error_msg(&wp->cw, "null recipient"); if (wp->dfgs & DFG_WORK_LOCK) lock_work(); return 0; } if (*user_lenp) { if (*user_lenp > RCTP_MAXNAME-1) *user_lenp = RCTP_MAXNAME-1; } else { /* Use the list of local domain names to guess a user name * from the recipient address * Take the whole recipient name if it does not * include a domain name */ atchr = memchr(addr, '@', addr_len); if (!atchr) { *userp = addr; *user_lenp = addr_len; return 1; } for (udom = user_domains; udom; udom = udom->fwd) { i = addr_len - udom->len; if (i > 0 && !strncasecmp(addr+i, udom->nm, udom->len)) { if (*udom->nm == '@' || *udom->nm == '.') { ; /* got it */ } else { /* match must start at '.' or '@' * if target did not start with * '.' or '@' then the name must * end with '.' or '@' */ if (i-- < 2 || (addr[i] != '.' && addr[i] != '@')) continue; } /* we have something like user or user@sub * from user@sub.domain.com */ *userp = addr; if (!udom->wildcard) { *user_lenp = i; } else { *user_lenp = atchr - addr; } return 1; } } } return 1; } /* The mutex must be locked */ static RCPT_ST * set_rcpt(WORK *wp, const char *rcpt, int rcpt_len, const char *user, int user_len) { RCPT_ST *rcpt_st; rcpt_st = alloc_rcpt_st(&wp->cw, 0); if (!rcpt_st) return 0; ++wp->cw.tgts; memcpy(rcpt_st->env_to, rcpt, rcpt_len); rcpt_st->env_to[rcpt_len] = '\0'; if (user_len) memcpy(rcpt_st->user, user, user_len); rcpt_st->user[user_len] = '\0'; rcpt_st->dir[0] = '\0'; if (user_len != 0 && !get_user_dir(rcpt_st, user, user_len, 0, 0)) thr_trace_msg(&wp->cw, "%s", wp->cw.emsg); return rcpt_st; } /* read lines of pairs if (env_To, username) values from the MTA */ static void get_ascii_rcpts(WORK *wp) { const char *addr, *user, *user_end; char c; int addr_len, user_len; RCPT_ST *rcpt_st; lock_work(); wp->dfgs |= DFG_WORK_LOCK; for (;;) { ascii_read_line(wp); /* stop after the empty line */ addr = wp->msg_rd.out; if (*addr == '\0') { wp->msg_rd.out = wp->msg_rd.next_line; break; } user_end = wp->msg_rd.next_line; while (user_end > addr && ((c = *(user_end-1)) == ' ' || c == '\t')) --user_end; /* bytes up to '\r' are the env_To value * and bytes after are the local recipient */ user = strchr(addr, '\r'); if (!user) { addr_len = user_end - wp->msg_rd.out; user = user_end; } else { addr_len = user - addr; ++user; } user_len = user_end - user; if (!check_addr(wp, &addr, &addr_len, &user, &user_len)) job_exit(wp); rcpt_st = set_rcpt(wp, addr, addr_len, user, user_len); if (!rcpt_st) job_exit(wp); /* Don't worry if this recipient is incompatible with preceding * recipients, because there is nothing we can do. * We cannot reject a recipient when the ASCII protocol is used, * but side effects of the check are required */ cmn_compat_whitelist(&wp->cw, rcpt_st); wp->msg_rd.out = wp->msg_rd.next_line; } unlock_work(); wp->dfgs &= ~DFG_WORK_LOCK; } /* We are finished with one SMTP message with the ASCII protocol * Send the result to the MTA and end this thread */ static void NRATTRIB ascii_done(WORK *wp, DCCIF_RESULT_CHAR result_char) { const char *result_str; RCPT_ST *rcpt_st; char *p; result_str = wp->cw.reply.log_result; if (!result_str) thr_error_msg(&wp->cw, "rejection reason undecided"); else thr_log_print(&wp->cw, 0, DCC_XHDR_RESULT"%s\n", result_str); /* tell MTA the overall result */ p = wp->msg_rd.base; *p++ = result_char; *p++ = '\n'; /* output list of recipients */ for (rcpt_st = wp->cw.rcpt_st_first; rcpt_st; rcpt_st = rcpt_st->fwd) { if (p >= &wp->msg_rd.base[wp->msg_rd.size-1]) { buf_write(wp, &wp->msg_wt, wp->msg_rd.base, wp->msg_rd.size-1); p = wp->msg_rd.base; } if (result_char == DCCIF_RESULT_GREY) { *p++ = DCCIF_RCPT_GREY; } else if (rcpt_st->fgs & RCPT_FG_ISSPAM) { *p++ = DCCIF_RCPT_REJECT; } else { *p++ = DCCIF_RCPT_ACCEPT; } } *p++ = '\n'; buf_write(wp, &wp->msg_wt, wp->msg_rd.base, p-wp->msg_rd.base); /* send the body from the temporary file to the MTA */ if (wp->dfgs & DFG_MTA_BODY) { body_copy(wp); } else if (wp->dfgs & DFG_MTA_HEADER) { /* MTA wants only the header, if we have it */ if (wp->cw.header.used != 0) buf_write(wp, &wp->msg_wt, wp->cw.header.buf, wp->cw.header.used); buf_write(wp, &wp->msg_wt, "\n", 1); } job_exit(wp); } /* use the dccifd ASCII protocol to talk to an MTA */ static void NRATTRIB ascii_job(WORK *wp) { # define AOPT(s) ascii_opt_str(wp, s, LITZ(s)) char *p; int i; log_start(&wp->cw); wp->msg_rd.base = wp->buf1; wp->msg_rd.size = sizeof(wp->buf1); wp->msg_rd.socp = &wp->proxy_in_soc; wp->msg_wt.base = wp->buf2; wp->msg_wt.size = sizeof(wp->buf2); wp->msg_wt.socp = &wp->proxy_in_soc; /* get any options */ ascii_read_line(wp); while (*wp->msg_rd.out != '\0') { if (AOPT(DCCIF_OPT_LOG)) { wp->cw.ask_st |= ASK_ST_LOGIT; continue; } if (AOPT(DCCIF_OPT_SPAM)) { wp->cw.ask_st |= ASK_ST_MTA_ISSPAM; continue; } if (AOPT(DCCIF_OPT_BODY)) { wp->dfgs |= DFG_MTA_BODY; continue; } if (AOPT(DCCIF_OPT_HEADER)) { wp->dfgs |= DFG_MTA_HEADER; continue; } if (AOPT(DCCIF_OPT_QUERY)) { wp->cw.ask_st |= ASK_ST_QUERY; continue; } if (AOPT(DCCIF_OPT_GREY_QUERY)) { wp->cw.ask_st |= ASK_ST_QUERY_GREY; continue; } if (AOPT(DCCIF_OPT_NO_REJECT)) { wp->cw.action = CMN_IGNORE; continue; } if (AOPT(DCCIF_OPT_RCVD_NXT) || AOPT(DCCIF_OPT_RCVD_NEXT)) { ++wp->parse_rcvd; continue; } thr_error_msg(&wp->cw, "unrecognized option value: \"%s\"", wp->msg_rd.out); job_exit(wp); } if ((wp->cw.ask_st & ASK_ST_MTA_ISSPAM) && (wp->cw.ask_st & ASK_ST_QUERY)) { thr_error_msg(&wp->cw, DCCIF_OPT_SPAM" and "DCCIF_OPT_QUERY " are incompatible"); wp->cw.ask_st &= ~ASK_ST_MTA_ISSPAM; } if ((wp->cw.ask_st & ASK_ST_MTA_ISSPAM) && (wp->cw.ask_st & ASK_ST_QUERY_GREY)) { thr_error_msg(&wp->cw, DCCIF_OPT_SPAM" and "DCCIF_OPT_GREY_QUERY " are incompatible"); wp->cw.ask_st &= ~ASK_ST_MTA_ISSPAM; } if (dcc_query_only && (wp->cw.ask_st & ASK_ST_MTA_ISSPAM)) { thr_error_msg(&wp->cw, DCCIF_OPT_SPAM" and -Q" " are incompatible"); wp->cw.ask_st &= ~ASK_ST_MTA_ISSPAM; } wp->msg_rd.out = wp->msg_rd.next_line; /* open the connection to the nearest DCC server * and figure out our X-DCC header */ if (!ck_dcc_ctxt(&wp->cw)) { /* failed to create context */ make_reply(&wp->cw.reply, &dcc_fail_reply, &wp->cw, 0); ascii_done(wp, DCCIF_RESULT_TEMP); } dcc_cks_init(&wp->cw.cks); dcc_dnsbl_init(&wp->cw.cks, wp->cw.dcc_ctxt, &wp->cw, wp->cw.id); wp->cw.cmn_fgs |= CMN_FG_LOG_EARLY; /* get the SMTP client IP address and host name */ i = ascii_read_line(wp); if (i == 0 || !strcmp("0.0.0.0", wp->msg_rd.out) || !strcmp("0.0.0.0\r0.0.0.0", wp->msg_rd.out)) { /* it is absent or the SpamAssassin junk */ wp->dfgs |= DFG_PARSE_RCVD; /* try a Received header */ } else { /* the host name follows the IP address */ p = strchr(wp->msg_rd.out, '\r'); if (p) { *p++ = '\0'; BUFCPY(wp->cw.clnt_name, p); } /* convert ASCCI representation of IP address to a * canonical form and to a checksum */ if (!dcc_get_str_ip_ck(&wp->cw.cks, wp->msg_rd.out)) { thr_error_msg(&wp->cw, "unrecognized IP address \"%s\"", wp->msg_rd.out); } else { /* convert IPv6 address to a canonical string */ wp->cw.clnt_addr = wp->cw.cks.ip_addr; dcc_ipv6tostr(wp->cw.clnt_str, sizeof(wp->cw.clnt_str), &wp->cw.clnt_addr); } } wp->msg_rd.out = wp->msg_rd.next_line; /* get the HELO value */ i = ascii_read_line(wp); get_helo(wp, wp->msg_rd.out, i); wp->msg_rd.out = wp->msg_rd.next_line; /* get the envelope Mail_From value */ i = ascii_read_line(wp); if (i > ISZ(wp->cw.env_from)-1) i = ISZ(wp->cw.env_from)-1; memcpy(wp->cw.env_from, wp->msg_rd.out, i); wp->cw.env_from[i] = '\0'; wp->msg_rd.out = wp->msg_rd.next_line; get_sender(wp); /* get the list of recipients from the MTA */ get_ascii_rcpts(wp); if (!get_body(wp)) { /* something wrong while collecting the message body * such as contacting the DCC server */ ascii_done(wp, DCCIF_RESULT_TEMP); } /* get consensus of targets' wishes */ users_process(&wp->cw); totals.tgts_rejected += wp->cw.reject_tgts; /* log the consensus & generate SMTP rejection message if needed */ users_log_result(&wp->cw, 0); if (wp->cw.ask_st & ASK_ST_GREY_EMBARGO) { totals.tgts_embargoed += wp->cw.tgts; ascii_done(wp, DCCIF_RESULT_GREY); } if (wp->cw.reject_tgts != 0 || (wp->cw.tgts == 0 && (wp->cw.ask_st & ASK_ST_CLNT_ISSPAM))) { if (wp->cw.action != CMN_IGNORE) { if (!wp->cw.reply.log_result) wp->cw.reply.log_result=DCC_XHDR_RESULT_REJECT; ascii_done(wp, DCCIF_RESULT_REJECT); } else { wp->cw.reply.log_result = DCC_XHDR_RESULT_I_A; ascii_done(wp, DCCIF_RESULT_OK); } } wp->cw.reply.log_result = DCC_XHDR_RESULT_ACCEPT; ascii_done(wp, DCCIF_RESULT_OK); # undef AOPT } #define SMTP_REPLY_221 "221 dccifd closing connection" #define SMTP_REPLY_220 "220 dccifd proxy ready" #define SMTP_REPLY_250_DATA "250 dccifd mail ok" #define SMTP_REPLY_250_RSET "250 dccifd RSET ok" #define SMTP_REPLY_250_HELO "250 dccifd HELO ok" #define SMTP_REPLY_250_POSTFIX "250 dccifd Postfix extension ok" #define SMTP_REPLY_250_RCPT "250 dccifd Recipient ok" #define SMTP_REPLY_250_MAIL "250 dccifd Sender ok" #define SMTP_REPLY_350 "354 Enter mail to dccifd" #define SMTP_REPLY_452_WLIST "452 4.5.3 "DCC_XHDR_INCOMPAT_WLIST #define SMTP_REPLY_452_2MANY "452 4.5.3 "DCC_XHDR_TOO_MANY_RCPTS #define SMTP_REPLY_500 "500 5.0.0 dccifd command unrecognized" #define SMTP_REPLY_501_NO_ARG "501 5.5.4 dccifd command arg required" #define SMTP_REPLY_501_BAD_ARG "501 5.5.1 dccifd command unrecognized" #define SMTP_REPLY_501_RCPT "501 5.5.2 dccifd RCPT command syntax error" #define SMTP_REPLY_501_POSTFIX "501 5.5.2 dccifd Postfix extension syntax error" #define SMTP_REPLY_503_DATA "503 5.0.0 dccifd need RCPT" #define SMTP_REPLY_503 "503 5.0.0 bad dccifd command sequence" #define SMTP_DEBUG_TRACE 3 static void smtp_trace(WORK *wp, const char *type, const char *buf, int len) { if (dcc_clnt_debug < SMTP_DEBUG_TRACE) return; log_start(&wp->cw); if (len > 0 && buf[len-1] == '\n') --len; if (len > 0 && buf[len-1] == '\r') --len; thr_trace_msg(&wp->cw, "%s %s: %.*s", wp->cw.id, type, len, buf); } /* send an SMTP reply upstream to the SMTP client of our proxy */ static void smtp_send_reply(WORK *wp, const char *reply, int len) { if (dcc_clnt_debug >= SMTP_DEBUG_TRACE) smtp_trace(wp, "SMTP response", reply, len); buf_write(wp, &wp->reply_out, reply, len); buf_write(wp, &wp->reply_out, "\r\n", 2); } #define SMTP_REPLY(r) smtp_send_reply(wp,SMTP_REPLY_##r,LITZ(SMTP_REPLY_##r)) static void smtp_reply_error(WORK *wp, const char *reply, int len) { smtp_send_reply(wp, reply, len); wp->msg_rd.out = wp->msg_rd.next_line; wp->smtp_state = SMTP_ST_ERROR; if (dcc_clnt_debug) wp->cw.ask_st |= ASK_ST_LOGIT; /* depend on '\n' ending the SMTP message */ thr_log_print(&wp->cw, 1, "%s", reply); } #define SMTP_ERROR(r) smtp_reply_error(wp,SMTP_REPLY_##r,LITZ(SMTP_REPLY_##r)) /* look for an SMTP verb */ typedef enum { SMTP_VERB_ERR, /* bad command */ SMTP_VERB_UNREC, /* unrecognized command */ SMTP_VERB_RSET, SMTP_VERB_HELO, SMTP_VERB_XCLIENT, /* Postfix extension */ SMTP_VERB_XFORWARD, /* Postfix extension */ SMTP_VERB_MAIL, /* Mail From */ SMTP_VERB_RCPT, /* Rcpt To */ SMTP_VERB_DATA, SMTP_VERB_QUIT } VERB_SMTP; typedef struct { const char *str; int str_len; const char *parm; int parm_len; VERB_SMTP verb; u_char arg_required; } PT; PT pt[] = { #define PT_M(s,p,v,r) {s,LITZ(s),p,LITZ(p),SMTP_VERB_##v,r} PT_M("HELO","", HELO, 1), PT_M("EHLO","", HELO, 1), PT_M("Mail","From:", MAIL, 1), PT_M("Rcpt","To:", RCPT, 1), PT_M("DATA","", DATA, 0), PT_M("XFORWARD","", XFORWARD, 1), PT_M("XCLIENT","", XCLIENT, 1), PT_M("RSET","", RSET, 0), PT_M("QUIT","", QUIT, 0), #undef PT_M }; #define RESP_DIGIT(c) ((c) >= '0' && (c) <= '9') static VERB_SMTP get_smtp_verb(WORK *wp, const char **ppp, /* parameter after command */ const char **pep) /* end of parameter */ { PT *ptp; const char *lp, *pp; int i, len; /* skip leading whitespace */ wp->msg_rd.out += strspn(wp->msg_rd.out, " \t"); lp = wp->msg_rd.out; if (dcc_clnt_debug >= SMTP_DEBUG_TRACE) smtp_trace(wp, "SMTP command", lp, wp->msg_rd.next_line - lp); for (ptp = pt; ptp < &pt[DIM(pt)]; ++ptp) { len = ptp->str_len; pp = lp+len; if (pp+2 > wp->msg_rd.next_line || strncasecmp(lp, ptp->str, len)) continue; if (pp+2 == wp->msg_rd.next_line) { /* SMTP command by itself on the line */ *ppp = 0; *pep = 0; if (ptp->arg_required) { SMTP_REPLY(501_NO_ARG); return SMTP_VERB_ERR; } /* finished, having matched the command */ return ptp->verb; } /* all other commands require following text separated * from the command with whitespace, as in "Mail From:" */ i = strspn(pp, " \t"); if (i == 0) continue; pp += i; /* skip the initial part of the parameter, such as "From:" */ if (ptp->parm_len) { if (*pp == '\r') { SMTP_REPLY(501_NO_ARG); return SMTP_VERB_ERR; } if (strncasecmp(pp, ptp->parm, ptp->parm_len)) { SMTP_REPLY(501_BAD_ARG); return SMTP_VERB_ERR; } pp += ptp->parm_len; pp += strspn(pp, " \t"); } *ppp = pp; *pep = strpbrk(pp, " \t\r"); return ptp->verb; } *ppp = 0; return SMTP_VERB_UNREC; } /* reset and restart the SMTP proxy state */ static void proxy_rset(WORK *wp) { if (wp->smtp_state == SMTP_ST_START) return; cmn_clear(&wp->cw, wp, 0); wp->dfgs &= DFG_RECYCLE; wp->smtp_state = SMTP_ST_START; dcc_cks_init(&wp->cw.cks); dcc_dnsbl_init(&wp->cw.cks, wp->cw.dcc_ctxt, &wp->cw, wp->cw.id); wp->cw.cmn_fgs |= CMN_FG_LOG_EARLY; /* Values from the Postfix XCLIENT ESMTP extension endure for the * entire session. Values from the XFORWARD extension or guesses from * a HELO command are cleared at the end of the SMTP transaction. */ if (!(wp->dfgs & DFG_XCLIENT_NAME)) wp->cw.clnt_name[0] = '\0'; if (wp->dfgs & DFG_XCLIENT_ADDR) { dcc_get_ipv6_ck(&wp->cw.cks, &wp->cw.clnt_addr); } else { memset(&wp->cw.clnt_addr, 0, sizeof(wp->cw.clnt_addr)); wp->cw.clnt_str[0] = '\0'; } if (!(wp->dfgs & DFG_XCLIENT_HELO)) wp->cw.helo[0] = '\0'; } /* deal with aborted SMTP sessions */ static void proxy_abort(WORK *wp, const char *log_msg) { if (dcc_clnt_debug >= SMTP_DEBUG_TRACE) smtp_trace(wp, "reset", log_msg, strlen(log_msg)); if (wp->smtp_state != SMTP_ST_START && wp->smtp_state != SMTP_ST_HELO && wp->smtp_state != SMTP_ST_ERROR) { wp->cw.ask_st |= ASK_ST_INVALID_MSG; if (!(wp->cw.cmn_fgs & CMN_FG_ENV_LOGGED)) thr_log_envelope(&wp->cw, 1); dcc_cks_fin(&wp->cw.cks); LOG_CAPTION(wp, "\n"DCC_LOG_MSG_SEP); thr_log_late(&wp->cw); cmn_ask_white(&wp->cw); users_process(&wp->cw); users_log_result(&wp->cw, log_msg); /* create log files for -d * and without any recipents but with "option log-all" */ if (dcc_clnt_debug || (wp->cw.init_sws & FLTR_SW_LOG_ALL)) wp->cw.ask_st |= ASK_ST_LOGIT; if (wp->cw.ask_st & ASK_ST_LOGIT) thr_log_print(&wp->cw, 0, DCC_XHDR_RESULT"%s\n", log_msg); } proxy_rset(wp); } /* quit a truncated message */ static void NRATTRIB proxy_msg_truncated(WORK *wp) { if (!proxy) { thr_error_msg(&wp->cw, "truncated message"); } else { proxy_abort(wp, "SMTP session aborted"); } job_exit(wp); } /* pass a line from the downstream proxy to our upstream */ static void smtp_pass_line(WORK *wp) { int len; len = wp->reply_in.next_line - wp->reply_in.out; if (dcc_clnt_debug >= SMTP_DEBUG_TRACE) smtp_trace(wp, "pass SMTP response", wp->reply_in.out, len); buf_write(wp, &wp->reply_out, wp->reply_in.out, wp->reply_in.next_line - wp->reply_in.out); wp->reply_in.out = wp->reply_in.next_line; } static int /* -1 or 1st digit of response */ smtp_get_resp(WORK *wp, u_char pass, /* 1=pass it upstream */ char *logbuf, /* log response here */ int logbuflen) { int len; char dig, cont; for (;;) { if (!msg_read_line(wp, &wp->reply_in)) { if (pass) job_exit(wp); return -1; } len = wp->reply_in.next_line - wp->reply_in.out; if (len < 5) { thr_error_msg(&wp->cw, "short SMTP response" " \"%s\" from proxy server", wp->reply_in.out); return -1; } dig = wp->reply_in.out[0]; cont = wp->reply_in.out[3]; if ((dig < '1' || dig > '5') || !RESP_DIGIT(wp->reply_in.out[1]) || !RESP_DIGIT(wp->reply_in.out[2]) || (cont != ' ' && cont != '-')) { *wp->reply_in.next_line = '\0'; thr_error_msg(&wp->cw, "unrecognized SMTP response" " \"%s\" from proxy", wp->reply_in.out); return -1; } if (logbuflen != 0) { if (logbuflen > len) logbuflen = len; memcpy(logbuf, wp->reply_in.out, logbuflen); while (logbuflen > 0 && (logbuf[logbuflen-1] == '\r' || logbuf[logbuflen-1] == '\n')) --logbuflen; logbuf[logbuflen] = '\0'; /* log only the first line */ logbuflen = 0; } if (pass) { /* pass the next line upstream */ smtp_pass_line(wp); if (cont == ' ') return dig; } else { /* we don't like multi-line responses */ if (cont == ' ') return dig; *wp->reply_in.next_line = '\0'; thr_error_msg(&wp->cw, "unacceptable multi-line" " SMTP response" " \"%s\" from proxy", wp->reply_in.out); return -1; } } } /* pass an SMTP command from upstream or the SMTP client of our proxy * downstream to the SMTP server of our proxy. * Then relay the SMTP server's response upstream to the SMTP client */ static u_char /* 0=server was unhappy */ smtp_pass_cmd(WORK *wp, const char *reply, /* send this response upstream if */ int reply_len, /* if downstream is happy */ char *logbuf, /* logbuf response here */ int logbuflen) { int len, result; /* fake it if the SMTP server is /dev/null */ if (proxy_out_family == AF_UNSPEC) { smtp_send_reply(wp, reply, reply_len); return 1; } len = wp->msg_rd.next_line - wp->msg_rd.out; if (len != 0) { buf_write(wp, &wp->msg_wt, wp->msg_rd.out, len); buf_write_flush(wp, &wp->msg_wt); } /* wait for & pass on response */ result = smtp_get_resp(wp, 1, logbuf, logbuflen); if (result < 0) job_exit(wp); return (result == reply[0]); } #define SMTP_PASS_CMD(r) smtp_pass_cmd(wp,SMTP_REPLY_##r,LITZ(SMTP_REPLY_##r), \ 0, 0) /* parse Postfix XFORWARD and XCLIENT commands * see http://www.postfix.org/XCLIENT_README.html * and http://www.postfix.org/XFORWARD_README.html * Depend on the Postfix downstream to answer EHLO with XFORWRD */ static void smtp_xpostfix(WORK *wp, VERB_SMTP verb, /* SMTP_VERB_XFORWARD or _XCLIENT */ const char *parm) { const char *val, *end_parm; char addr[INET6_ADDRSTRLEN+1]; int i; wp->smtp_state = SMTP_ST_HELO; for (; ; parm = end_parm + strspn(end_parm, " \t")) { if (*parm == '\r' || *parm == '\n') { SMTP_PASS_CMD(250_POSTFIX); break; } /* find the value */ val = strpbrk(parm, "= \t\r\n"); if (*val++ != '=') { SMTP_REPLY(501_POSTFIX); break; } end_parm = strpbrk(val, " \t\r\n"); if (!CLITCMP(parm, "NAME=")) { if (!CLITCMP(val, "[UNAVAILABLE]") || !CLITCMP(val, "[TEMPUNAVAIL]")) { wp->cw.clnt_name[0] = '\0'; continue; } i = min(ISZ(wp->cw.clnt_name)-1, end_parm-val); memcpy(wp->cw.clnt_name, val, i); wp->cw.clnt_name[i] = '\0'; if (verb == SMTP_VERB_XCLIENT) wp->dfgs |= DFG_XCLIENT_NAME; else wp->dfgs &= ~DFG_XCLIENT_NAME; continue; } if (!CLITCMP(parm, "ADDR=")) { if (!CLITCMP(val, "[UNAVAILABLE]") || !CLITCMP(val, "[TEMPUNAVAIL]")) { wp->cw.clnt_str[0] = '\0'; continue; } if (!CLITCMP(val, "IPV6")) val += LITZ("IPV6"); i = min(ISZ(addr)-1, end_parm-val); memcpy(addr, val, i); addr[i] = '\0'; /* try to convert ASCCI representation of IP address to * a canonical form and to a checksum */ if (!dcc_get_str_ip_ck(&wp->cw.cks, addr)) { thr_error_msg(&wp->cw, "unrecognized IP address \"%s\"", addr); wp->cw.clnt_str[0] = '\0'; continue; } wp->cw.clnt_addr = wp->cw.cks.ip_addr; dcc_ipv6tostr(wp->cw.clnt_str, sizeof(wp->cw.clnt_str), &wp->cw.clnt_addr); if (verb == SMTP_VERB_XCLIENT) { wp->dfgs |= DFG_XCLIENT_ADDR; } else { wp->dfgs &= ~DFG_XCLIENT_ADDR; } continue; } if (!CLITCMP(parm, "HELO=")) { if (!CLITCMP(val, "[UNAVAILABLE]") || !CLITCMP(val, "[TEMPUNAVAIL]")) { wp->cw.helo[0] = '\0'; } else { get_helo(wp, val, end_parm-val); } if (verb == SMTP_VERB_XCLIENT) wp->dfgs |= DFG_XCLIENT_HELO; else wp->dfgs &= ~DFG_XCLIENT_HELO; continue; } } wp->msg_rd.out = wp->msg_rd.next_line; } /* parse SMTP Rcpt_To command */ static u_char /* 1=now seen >=1 good Rcpt command*/ get_smtp_rcpt(WORK *wp, const char *path, const char *epath) { const char *user; int path_len, user_len; RCPT_ST *rcpt_st; path_len = epath - path; user = ""; user_len = 0; if (!check_addr(wp, &path, &path_len, &user, &user_len)) { SMTP_REPLY(501_RCPT); wp->msg_rd.out = wp->msg_rd.next_line; return 0; } lock_work(); wp->dfgs |= DFG_WORK_LOCK; rcpt_st = set_rcpt(wp, path, path_len, user, user_len); unlock_work(); wp->dfgs &= ~DFG_WORK_LOCK; if (!rcpt_st) { SMTP_REPLY(452_2MANY); wp->msg_rd.out = wp->msg_rd.next_line; return 0; } /* if this recipient is incompatible with preceding recipients * then reject it and remember to put something into the log */ if (!cmn_compat_whitelist(&wp->cw, rcpt_st)) { --wp->cw.tgts; SMTP_REPLY(452_WLIST); wp->msg_rd.out = wp->msg_rd.next_line; BUFCPY(rcpt_st->rej_msg, SMTP_REPLY_452_WLIST); rcpt_st->rej_result = DCC_XHDR_INCOMPAT_WLIST; rcpt_st->fgs |= RCPT_FG_REJ_FILTER; return 0; } /* Try to pass the command in the buffer downstream. * Forget this recipient if it is not good enough downstream. * Let the downstream proxy do its own logging. * That way we need save only one bit instead of an SMTP * rejection message for our eventual per-user log file. */ if (!smtp_pass_cmd(wp, SMTP_REPLY_250_RCPT, LITZ(SMTP_REPLY_250_RCPT), rcpt_st->rej_msg, sizeof(rcpt_st->rej_msg))) { --wp->cw.tgts; wp->msg_rd.out = wp->msg_rd.next_line; rcpt_st->rej_result = rcpt_st->rej_msg; rcpt_st->rej_result += strspn(rcpt_st->rej_result, "0123456789. "); if (rcpt_st->rej_msg[0] != '4') ++wp->cw.mta_rej_tgts; wp->cw.ask_st |= ASK_ST_LOGIT; rcpt_st->fgs |= RCPT_FG_REJ_FILTER; return 0; } /* there was no problem if the downstream was happy */ rcpt_st->rej_msg[0] = '\0'; wp->msg_rd.out = wp->msg_rd.next_line; return 1; } static void smtp_data_abort(WORK *wp) { /* After a rejection Postfix wants a before-queue proxy to * "abort the connnection" to the SMTP server downstream. * That might mean a simple TCP shutdown, but for testing with * sendmail, send an SMTP RSET command. */ if (proxy_out_family != AF_UNSPEC) { buf_write(wp, &wp->msg_wt, "RSET\r\n", LITZ("RSET\r\n")); buf_write_flush(wp, &wp->msg_wt); /* sendmail will respond, but what about Postfix? */ if (smtp_get_resp(wp, 0, 0, 0) >= 0 && dcc_clnt_debug >= SMTP_DEBUG_TRACE) { smtp_trace(wp, "ignore response to RSET", wp->reply_in.out, wp->reply_in.next_line - wp->reply_in.out); wp->reply_in.out = wp->reply_in.next_line; } } } /* We are rejecting the transaction with our generated reply */ static void smtp_trans_reject(WORK *wp) { if (dcc_clnt_debug >= SMTP_DEBUG_TRACE) thr_trace_msg(&wp->cw, "%s SMTP response: %s %s %s", wp->cw.id, wp->cw.reply.rcode, wp->cw.reply.xcode, wp->cw.reply.str); if (wp->proxy_in_soc >= 0) { buf_write(wp, &wp->reply_out, wp->cw.reply.rcode, strlen(wp->cw.reply.rcode)); buf_write(wp, &wp->reply_out, " ", 1); buf_write(wp, &wp->reply_out, wp->cw.reply.xcode, strlen(wp->cw.reply.xcode)); buf_write(wp, &wp->reply_out, " ", 1); buf_write(wp, &wp->reply_out, wp->cw.reply.str, strlen(wp->cw.reply.str)); buf_write(wp, &wp->reply_out, "\r\n", 2); buf_write_flush(wp, &wp->reply_out); log_smtp_reply(&wp->cw); } if (wp->proxy_out_soc >= 0) smtp_data_abort(wp); } static void NRATTRIB smtp_temp_fail(WORK *wp) { make_reply(&wp->cw.reply, &dcc_fail_reply, &wp->cw, 0); smtp_trans_reject(wp); job_exit(wp); } /* Send the mail message to the SMTP server for our proxy. */ static const char * smtp_data_send(WORK *wp) { int class; int len; const char *result; /* First send the DATA command. */ buf_write(wp, &wp->msg_wt, "DATA\r\n", LITZ("DATA\r\n")); buf_write_flush(wp, &wp->msg_wt); /* the server should respond with a 350 result */ class = smtp_get_resp(wp, 0, 0, 0); if (class < 0) job_exit(wp); if (class == '3') { wp->reply_in.out = wp->reply_in.next_line; /* send the mail message to the SMTP server */ body_copy(wp); buf_write(wp, &wp->msg_wt, ".\r\n", 3); buf_write_flush(wp, &wp->msg_wt); /* see what it has to say */ class = smtp_get_resp(wp, 0, 0, 0); } else if (class != '4' && class != '5') { /* or quit if response to DATA was not 4yz or 5yz */ thr_error_msg(&wp->cw, "unrecognized SMTP response" " \"%s\" from proxy to DATA command", wp->reply_in.out); job_exit(wp); } if (class == '2') { result = DCC_XHDR_RESULT_ACCEPT; } else { len = wp->reply_in.next_line - wp->reply_in.out; while (len > 0 && (wp->reply_in.out[len-1] == '\r' || wp->reply_in.out[len-1] == '\n')) --len; if (len > ISZ(wp->cw.reply.str)-2) len = ISZ(wp->cw.reply.str)-2; memcpy(wp->cw.reply.str_buf, wp->reply_in.out, len); wp->cw.reply.str_buf[len++] = '\n'; wp->cw.reply.str_buf[len] = '\0'; wp->cw.reply.str = wp->cw.reply.str_buf; thr_log_print(&wp->cw, 0, "SMTP server "DCC_XHDR_REJ_DATA_MSG"%.*s", len, wp->cw.reply.str); if (dcc_clnt_debug) wp->cw.ask_st |= ASK_ST_LOGIT; result = "rejected by SMTP server"; } /* pass SMTP server's response to the SMTP client */ smtp_pass_line(wp); return result; } static void smtp_data(WORK *wp) { const char *global_result, *user_result; if (!wp->cw.num_rcpts) { /* never got SMTP DATA command */ SMTP_ERROR(503_DATA); return; } /* tell STMP client to send the mail message */ wp->msg_rd.out = wp->msg_rd.next_line; SMTP_REPLY(350); buf_write_flush(wp, &wp->reply_out); if (!get_body(wp)) { /* something went wrong while collecting the message body * such as contacting the DCC server */ smtp_temp_fail(wp); return; } /* get consensus of targets' wishes */ users_process(&wp->cw); if (wp->cw.ask_st & ASK_ST_GREY_EMBARGO) { totals.tgts_embargoed += wp->cw.tgts; smtp_trans_reject(wp); global_result = wp->cw.reply.log_result; user_result = global_result; } else if (wp->cw.reject_tgts != 0) { switch (wp->cw.action) { case CMN_IGNORE: totals.tgts_ignored += wp->cw.reject_tgts; ++totals.msgs_spam; if (proxy_out_family == AF_UNSPEC) { SMTP_REPLY(250_DATA); global_result = DCC_XHDR_RESULT_I_A; } else { global_result = smtp_data_send(wp); } user_result = global_result; break; case CMN_DISCARD: totals.tgts_discarded += wp->cw.reject_tgts; ++totals.msgs_spam; /* discard the message by aborting the downstream * SMTP session and telling the upstream 250-OK */ if (proxy_out_family != AF_UNSPEC) smtp_data_abort(wp); SMTP_REPLY(250_DATA); global_result = DCC_XHDR_RESULT_DISCARD; user_result = global_result; break; case CMN_REJECT: default: totals.tgts_rejected += wp->cw.reject_tgts; ++totals.msgs_spam; smtp_trans_reject(wp); global_result = wp->cw.reply.log_result; user_result = 0; break; } } else if (proxy_out_family == AF_UNSPEC) { SMTP_REPLY(250_DATA); global_result = DCC_XHDR_RESULT_ACCEPT; user_result = 0; } else { global_result = smtp_data_send(wp); user_result = 0; } /* log the consensus & generate SMTP rejection message if needed */ users_log_result(&wp->cw, user_result); thr_log_print(&wp->cw, 0, DCC_XHDR_RESULT"%s\n", global_result); proxy_rset(wp); } /* use a subset of SMTP to talk to an MTA */ static void NRATTRIB proxy_job(WORK *wp) { char str[DCC_SU2STR_SIZE]; DCC_SOCKU su; int out_soc; VERB_SMTP verb; const char *parmp, *eparmp; int i; wp->msg_rd.base = wp->buf1; wp->msg_rd.size = sizeof(wp->buf1); wp->msg_rd.socp = &wp->proxy_in_soc; wp->msg_wt.base = wp->buf2; wp->msg_wt.size = sizeof(wp->buf2); wp->msg_wt.socp = &wp->proxy_out_soc; wp->reply_in.base = wp->buf3; wp->reply_in.size = sizeof(wp->buf3); wp->reply_in.socp = &wp->proxy_out_soc; wp->reply_out.base = wp->buf4; wp->reply_out.size = sizeof(wp->buf4); wp->reply_out.socp = &wp->proxy_in_soc; if (proxy_out_family == AF_UNSPEC) { /* single-ended */ ; } else if (proxy_out_family == AF_UNIX) { out_soc = socket(AF_UNIX, SOCK_STREAM, 0); if (out_soc < 0) { thr_error_msg(&wp->cw, "proxy_out socket(AF_UNIX): %s", ERROR_STR()); smtp_temp_fail(wp); } if (0 > connect(out_soc, (struct sockaddr *)&proxy_out_sun, sizeof(listen_sun))) { thr_error_msg(&wp->cw, "proxy_out connect(AF_UNIX,%s): %s", proxy_out_sun.sun_path, ERROR_STR()); close(out_soc); smtp_temp_fail(wp); } if (!set_soc(wp->cw.emsg, out_soc, AF_UNIX, "proxy output")) { thr_error_msg(&wp->cw, "%s", wp->cw.emsg); close(out_soc); smtp_temp_fail(wp); } wp->proxy_out_soc = out_soc; wp->dfgs |= DFG_MTA_BODY; } else { if (proxy_out_su.sa.sa_family == AF_UNSPEC) { /* use SMTP client's IP address */ if (wp->proxy_in_su.sa.sa_family == AF_INET) dcc_mk_su(&su, AF_INET, &wp->proxy_in_su.ipv4.sin_addr, proxy_out_port); else dcc_mk_su(&su, AF_INET6, &wp->proxy_in_su.ipv6.sin6_addr, proxy_out_port); } else { su = proxy_out_su; } out_soc = socket(su.sa.sa_family, SOCK_STREAM, 0); if (out_soc < 0) { thr_error_msg(&wp->cw, "proxy_out socket(%s): %s", dcc_su2str(str, sizeof(str), &su), ERROR_STR()); close(out_soc); smtp_temp_fail(wp); } if (0 > connect(out_soc, &su.sa, DCC_SU_LEN(&su))) { thr_error_msg(&wp->cw, "proxy_out connect(%s): %s", dcc_su2str(str, sizeof(str), &su), ERROR_STR()); close(out_soc); smtp_temp_fail(wp); } if (!set_soc(wp->cw.emsg, out_soc, su.sa.sa_family, "proxy output")) { thr_error_msg(&wp->cw, "%s", wp->cw.emsg); close(out_soc); smtp_temp_fail(wp); } wp->proxy_out_soc = out_soc; wp->dfgs |= DFG_MTA_BODY; } /* open the connection to the nearest DCC server * and figure out our X-DCC header */ if (!ck_dcc_ctxt(&wp->cw)) { /* failed to create context */ smtp_temp_fail(wp); } dcc_cks_init(&wp->cw.cks); dcc_dnsbl_init(&wp->cw.cks, wp->cw.dcc_ctxt, &wp->cw, wp->cw.id); wp->cw.cmn_fgs |= CMN_FG_LOG_EARLY; /* pass the banner upstream */ wp->smtp_state = SMTP_ST_START; SMTP_PASS_CMD(220); /* assume for now that the IP address is the SMTP client to our proxy */ if (wp->proxy_in_su.sa.sa_family != AF_UNIX) { if (wp->proxy_in_su.sa.sa_family == AF_INET) { dcc_ipv4toipv6(&wp->cw.clnt_addr, wp->proxy_in_su.ipv4.sin_addr); } else { wp->cw.clnt_addr = (wp->proxy_in_su.ipv6.sin6_addr); } dcc_ipv6tostr(wp->cw.clnt_str, sizeof(wp->cw.clnt_str), &wp->cw.clnt_addr); } for (;;) { buf_write_flush(wp, &wp->reply_out); if (!msg_read_line(wp, &wp->msg_rd)) { proxy_abort(wp, "SMTP session aborted"); job_exit(wp); } verb = get_smtp_verb(wp, &parmp, &eparmp); /* deal with common SMTP commands */ switch (verb) { case SMTP_VERB_ERR: wp->msg_rd.out = wp->msg_rd.next_line; continue; case SMTP_VERB_UNREC: /* pass anything we don't recognize to the SMTP server * for our proxy. * If we don't have a server, reject it */ SMTP_PASS_CMD(500); wp->msg_rd.out = wp->msg_rd.next_line; continue; case SMTP_VERB_QUIT: SMTP_PASS_CMD(221); proxy_abort(wp, "SMTP session aborted with QUIT"); job_exit(wp); case SMTP_VERB_RSET: SMTP_PASS_CMD(250_RSET); proxy_abort(wp, "SMTP transaction aborted with RSET"); wp->msg_rd.out = wp->msg_rd.next_line; continue; case SMTP_VERB_HELO: proxy_abort(wp, "SMTP transaction aborted with HELO"); wp->smtp_state = SMTP_ST_HELO; /* save helo value in case there is no XPOSTFIX */ if (SMTP_PASS_CMD(250_HELO) && wp->cw.helo[0] == '\0' && !(wp->dfgs & DFG_XCLIENT_HELO)) get_helo(wp, parmp, eparmp - parmp); wp->msg_rd.out = wp->msg_rd.next_line; continue; case SMTP_VERB_XCLIENT: case SMTP_VERB_XFORWARD: case SMTP_VERB_MAIL: case SMTP_VERB_RCPT: case SMTP_VERB_DATA: break; } switch (wp->smtp_state) { case SMTP_ST_START: /* expecting HELO or perhaps MAIL */ case SMTP_ST_HELO: /* seen HELO */ if (verb == SMTP_VERB_XCLIENT || verb == SMTP_VERB_XFORWARD) { smtp_xpostfix(wp, verb, parmp); continue; } if (verb == SMTP_VERB_MAIL) { /* This might be a second message for the * connection. * Get sender, start logging, etc. at * the Mail_From command. */ if (SMTP_PASS_CMD(250_MAIL)) { log_start(&wp->cw); i = eparmp - parmp; if (i > ISZ(wp->cw.env_from)-1) i = ISZ(wp->cw.env_from)-1; memcpy(wp->cw.env_from, parmp, i); wp->cw.env_from[i] = '\0'; wp->smtp_state = SMTP_ST_TRANS; } wp->msg_rd.out = wp->msg_rd.next_line; get_sender(wp); continue; } break; case SMTP_ST_TRANS: /* seen Mail_From */ if (verb == SMTP_VERB_RCPT) { if (get_smtp_rcpt(wp, parmp, eparmp)) wp->smtp_state = SMTP_ST_RCPT; continue; } break; case SMTP_ST_RCPT: /* seen Rctp_To */ if (verb == SMTP_VERB_RCPT) { get_smtp_rcpt(wp, parmp, eparmp); continue; } if (verb == SMTP_VERB_DATA) { smtp_data(wp); continue; } break; case SMTP_ST_ERROR: /* no transaction until RSET or HELO */ break; } /* reject anything out of sequence */ SMTP_ERROR(503); } } /* start a new connection to an MTA */ static void * NRATTRIB job_start(void *wp0) { WORK *wp = wp0; /* working threads do not deal with signals */ clnt_sigs_off(0); if (proxy) proxy_job(wp); else ascii_job(wp); }