/* * Copyright (c) 2004, Stefan Walter * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above * copyright notice, this list of conditions and the * following disclaimer. * * Redistributions in binary form must reproduce the * above copyright notice, this list of conditions and * the following disclaimer in the documentation and/or * other materials provided with the distribution. * * The names of contributors to this software may not be * used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. * * * CONTRIBUTORS * Stefan Walter * Andreas Steinmetz * Rubio Vaughan * Olivier Beyssac */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "usuals.h" #if LINUX_NETFILTER #include #include #endif #ifdef HAVE_LIBCAP #include #include #endif #ifdef USE_PF_NATLOOKUP #include #include #include #include #endif /* USE_PF_NATLOOKUP */ #include "compat.h" #include "sock_any.h" #include "stringx.h" #include "sppriv.h" /* ----------------------------------------------------------------------- * STRUCTURES */ typedef struct spthread { pthread_t tid; /* Written to by the main thread */ int fd; /* The file descriptor or -1 */ } spthread_t; /* ----------------------------------------------------------------------- * DATA */ #define CRLF "\r\n" #define SMTP_TOOLONG "500 Line too long" CRLF #define SMTP_STARTBUSY "421 Server busy, too many connections" CRLF #define SMTP_STARTFAILED "421 Local Error, cannot start thread" CRLF #define SMTP_DATAINTERMED "354 Start mail input; end with ." CRLF #define SMTP_FAILED "451 Local Error" CRLF #define SMTP_NOTSUPP "502 Command not implemented" CRLF #define SMTP_NOTAUTH "554 Insufficient authorization" CRLF #define SMTP_OK "250 Ok" CRLF #define SMTP_REJPREFIX "550 Content Rejected; " #define SMTP_DATA "DATA" CRLF #define SMTP_NOOP "NOOP" CRLF #define SMTP_RSET "RSET" CRLF #define SMTP_XCLIENT "XCLIENT ADDR=%s" CRLF #define SMTP_BANNER "220 smtp.passthru" CRLF #define SMTP_HELO_RSP "250 smtp.passthru" CRLF #define SMTP_EHLO_RSP "250-smtp.passthru" CRLF #define SMTP_FEAT_RSP "250 XFILTERED" CRLF #define SMTP_DELIMS "\r\n\t :" #define SMTP_MULTI_DELIMS " -" #define ESMTP_PIPELINE "PIPELINING" #define ESMTP_TLS "STARTTLS" #define ESMTP_CHUNK "CHUNKING" #define ESMTP_BINARY "BINARYMIME" #define ESMTP_CHECK "CHECKPOINT" #define ESMTP_XCLIENT "XCLIENT" #define ESMTP_XEXCH50 "XEXCH50" #define HELO_CMD "HELO" #define EHLO_CMD "EHLO" #define FROM_CMD "MAIL FROM" #define TO_CMD "RCPT TO" #define DATA_CMD "DATA" #define RSET_CMD "RSET" #define STARTTLS_CMD "STARTTLS" #define BDAT_CMD "BDAT" #define XCLIENT_CMD "XCLIENT" #define XFORWARD_CMD "XFORWARD" #define AUTH_CMD "AUTH" #define DATA_END_SIG "." CRLF #define DATA_RSP "354" #define OK_RSP "250" #define START_RSP "220" #define AUTH_SUCCESS_RSP "235" #define RCVD_HEADER "Received:" /* The set of delimiters that can be present between config and value */ #define CFG_DELIMS ": \t" /* Maximum length of the header argument */ #define MAX_HEADER_LENGTH 1024 /* Maximum number of concurrent connections */ #define TOP_MAX_CONNECTIONS 10240 /* * asctime_r manpage: "stores the string in a user-supplied buffer of * length at least 26". We'll need some more bytes to put timezone * information behind */ #define MAX_DATE_LENGTH 64 #define LINE_TOO_LONG(l) ((l) >= (SP_LINE_LENGTH - 2)) /* ----------------------------------------------------------------------- * CONFIGURATION OPTIONS * * - Be sure that your configuration option needs to go into this * file. More likely it'll go into clamsmtpd.c * - When adding configuration options follow the instructions in * clamsmtpd.c, except add option to spstate_t (sppriv.h) and parse in * sp_parse_option (below) */ #define CFG_MAXTHREADS "MaxConnections" #define CFG_TIMEOUT "TimeOut" #define CFG_OUTADDR "OutAddress" #define CFG_LISTENADDR "Listen" #define CFG_TRANSPARENT "TransparentProxy" #define CFG_DIRECTORY "TempDirectory" #define CFG_KEEPALIVES "KeepAlives" #define CFG_USER "User" #define CFG_PIDFILE "PidFile" #define CFG_XCLIENT "XClient" #define CFG_SKIP "Skip" #define VAL_AUTHENTICATED "authenticated" #define VAL_CLIENT "client" #define VAL_FULL "full" /* ----------------------------------------------------------------------- * DEFAULT SETTINGS */ #define DEFAULT_SOCKET "10025" #define DEFAULT_PORT 10025 #define DEFAULT_MAXTHREADS 64 #define DEFAULT_TIMEOUT 180 #define DEFAULT_KEEPALIVES 0 /* ----------------------------------------------------------------------- * GLOBALS */ spstate_t g_state; /* The state and configuration of the daemon */ unsigned int g_unique_id = 0x00100000; /* For connection ids */ pthread_mutex_t g_mutex; /* The main mutex */ pthread_mutexattr_t g_mtxattr; /* ----------------------------------------------------------------------- * FORWARD DECLARATIONS */ static void on_quit(int signal); static void drop_privileges(); static void pid_file(int write); static void connection_loop(int sock); static void* thread_main(void* arg); static int smtp_passthru(spctx_t* ctx); static int make_connections(spctx_t* ctx, int client); static int read_server_response(spctx_t* ctx); static int parse_config_file(const char* configfile); static char* parse_address(char* line); static char* parse_xforward(char* line, const char* part); static const char* get_successful_rsp(const char* line, int* cont); static void do_server_noop(spctx_t* ctx); /* Used externally in some cases */ int sp_parse_option(const char* name, const char* option); /* ---------------------------------------------------------------------------------- * BASIC RUN FUNCTIONALITY */ void sp_init(const char* name) { int r; ASSERT(name); memset(&g_state, 0, sizeof(g_state)); sp_message(NULL, LOG_DEBUG, "%s (%s)", name, VERSION); /* Setup the defaults */ g_state.debug_level = -1; g_state.max_threads = DEFAULT_MAXTHREADS; g_state.timeout.tv_sec = DEFAULT_TIMEOUT; g_state.keepalives = DEFAULT_KEEPALIVES; g_state.directory = _PATH_TMP; g_state.name = name; /* We need the default to parse into a useable form, so we do this: */ r = sp_parse_option(CFG_LISTENADDR, DEFAULT_SOCKET); ASSERT(r == 1); /* Create the main mutex and condition variable */ if(pthread_mutexattr_init(&g_mtxattr) != 0 || #ifdef HAVE_ERR_MUTEX pthread_mutexattr_settype(&g_mtxattr, MUTEX_TYPE) || #endif pthread_mutex_init(&g_mutex, &g_mtxattr) != 0) errx(1, "threading problem. can't create mutex or condition var"); } int sp_run(const char* configfile, const char* pidfile, int dbg_level) { int sock; int true = 1; ASSERT(configfile); ASSERT(g_state.name); if(!(dbg_level == -1 || dbg_level <= LOG_DEBUG)) errx(2, "invalid debug log level (must be between 1 and 4)"); g_state.debug_level = dbg_level; g_state.pidfile = pidfile; /* Now parse the configuration file */ if(parse_config_file(configfile) == -1) { /* * We used to do a check here before whether it was the default * configuration file or not, but we can't do that any longer * as it comes from the app. Usually lack of a configuration * file will cause the following checks to fail */ warnx("configuration file not found: %s", configfile); } /* This option has no default, but is required ... */ if(g_state.outname == NULL && !g_state.transparent) errx(2, "no " CFG_OUTADDR " specified."); /* ... unless we're in transparent proxy mode */ else if(g_state.outname != NULL && g_state.transparent) warnx("the " CFG_OUTADDR " option will be ignored when " CFG_TRANSPARENT " is enabled"); sp_messagex(NULL, LOG_DEBUG, "starting up (%s)...", VERSION); /* Drop privileges before daemonizing */ drop_privileges(); /* When set to this we daemonize */ if(g_state.debug_level == -1) { /* Fork a daemon nicely here */ if(daemon(0, 0) == -1) { sp_message(NULL, LOG_ERR, "couldn't run as daemon"); exit(1); } sp_messagex(NULL, LOG_DEBUG, "running as a daemon"); g_state.daemonized = 1; /* Open the system log */ openlog(g_state.name, 0, LOG_MAIL); } /* Handle some signals */ signal(SIGPIPE, SIG_IGN); signal(SIGHUP, SIG_IGN); signal(SIGINT, on_quit); signal(SIGTERM, on_quit); siginterrupt(SIGINT, 1); siginterrupt(SIGTERM, 1); /* Create the socket */ sock = socket(SANY_TYPE(g_state.listenaddr), SOCK_STREAM, 0); if(sock < 0) { sp_message(NULL, LOG_CRIT, "couldn't open socket"); exit(1); } fcntl(sock, F_SETFD, fcntl(sock, F_GETFD, 0) | FD_CLOEXEC); setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (void *)&true, sizeof(true)); /* Unlink the socket file if it exists */ if(SANY_TYPE(g_state.listenaddr) == AF_UNIX) unlink(g_state.listenname); #ifdef HAVE_IP_TRANSPARENT if(g_state.transparent == TRANSPARENT_FULL) { int value = 1; if(setsockopt(sock, SOL_IP, IP_TRANSPARENT, &value, sizeof(value)) < 0) sp_message(NULL, LOG_WARNING, "couldn't set transparent mode on socket"); } #endif if(bind(sock, &SANY_ADDR(g_state.listenaddr), SANY_LEN(g_state.listenaddr)) != 0) { sp_message(NULL, LOG_CRIT, "couldn't bind to address: %s", g_state.listenname); exit(1); } sp_messagex(NULL, LOG_DEBUG, "created socket: %s", g_state.listenname); /* Let 5 connections queue up */ if(listen(sock, 5) != 0) { sp_message(NULL, LOG_CRIT, "couldn't listen on socket"); exit(1); } pid_file(1); sp_messagex(NULL, LOG_DEBUG, "accepting connections"); connection_loop(sock); pid_file(0); /* Our listen socket */ close(sock); sp_messagex(NULL, LOG_DEBUG, "stopped processing"); return 0; } void sp_quit() { /* The handler sets the flag and this also interrupts io */ kill(getpid(), SIGTERM); } int sp_is_quit() { return g_state.quit ? 1 : 0; } void sp_done() { /* Close the mutex */ pthread_mutex_destroy(&g_mutex); pthread_mutexattr_destroy(&g_mtxattr); if(g_state._p) free(g_state._p); memset(&g_state, 0, sizeof(g_state)); } static void on_quit(int signal) { g_state.quit = 1; } static void drop_privileges() { char* t; struct passwd* pw; uid_t uid; #ifdef HAVE_LIBCAP cap_t caps; cap_value_t value; #endif if(g_state.user) { if(geteuid() != 0) { sp_messagex(NULL, LOG_WARNING, "must be started as root to switch to user: %s", g_state.user); return; } #ifdef HAVE_LIBCAP /* * Tell kernel not clear capabilities when dropping root. * We drop them manually below, while keeping a small set. */ if(prctl(PR_SET_KEEPCAPS, 1) < 0) sp_message (NULL, LOG_WARNING, "couldn't keep capabilities when dropping privileges"); #endif uid = strtol(g_state.user, &t, 10); if(!t[0]) /* successful parse */ pw = getpwuid(uid); else /* must be a name */ pw = getpwnam(g_state.user); if(pw == NULL) errx(1, "couldn't look up user: %s", g_state.user); if(setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) == -1 || setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) == -1) err(1, "unable to switch to user: %s (uid %d, gid %d)", g_state.user, pw->pw_uid, pw->pw_gid); #ifdef HAVE_LIBCAP /* * No capabilities have been dropped yet. So we drop them here * keeping only those we need for our proxying. */ caps = cap_init(); if (caps == NULL) err(1, "unable to init capabilities"); value = CAP_NET_ADMIN; if (cap_set_flag(caps, CAP_EFFECTIVE, 1, &value, CAP_SET) < 0 || cap_set_flag(caps, CAP_PERMITTED, 1, &value, CAP_SET) < 0 || cap_set_proc(caps) < 0) err(1, "couldn't set capabilities when switching user"); cap_free (caps); #endif /* A paranoia check */ if(setreuid(-1, 0) == 0) err(1, "unable to completely drop privileges"); sp_messagex(NULL, LOG_DEBUG, "switched to user %s (uid %d, gid %d)", g_state.user, pw->pw_uid, pw->pw_gid); } if(geteuid() == 0) sp_messagex(NULL, LOG_WARNING, "running as root is NOT recommended"); } static void pid_file(int write) { if(!g_state.pidfile) return; if(write) { FILE* f = fopen(g_state.pidfile, "w"); if(f == NULL) { sp_message(NULL, LOG_ERR, "couldn't open pid file: %s", g_state.pidfile); } else { fprintf(f, "%d\n", (int)getpid()); if(ferror(f)) sp_message(NULL, LOG_ERR, "couldn't write to pid file: %s", g_state.pidfile); if(fclose(f) == EOF) sp_message(NULL, LOG_ERR, "couldn't write to pid file: %s", g_state.pidfile); } sp_messagex(NULL, LOG_DEBUG, "wrote pid file: %s", g_state.pidfile); } else { unlink(g_state.pidfile); sp_messagex(NULL, LOG_DEBUG, "removed pid file: %s", g_state.pidfile); } } static void connection_loop(int sock) { spthread_t* threads = NULL; int fd, i, x, r; /* Create the thread buffers */ threads = (spthread_t*)calloc(g_state.max_threads, sizeof(spthread_t)); if(!threads) { sp_messagex(NULL, LOG_CRIT, "out of memory"); return; } /* Now loop and accept the connections */ while(!sp_is_quit()) { fd = accept(sock, NULL, NULL); if(fd == -1) { switch(errno) { case EINTR: case EAGAIN: break; case ECONNABORTED: sp_message(NULL, LOG_ERR, "couldn't accept a connection"); break; default: sp_message(NULL, LOG_ERR, "couldn't accept a connection"); break; }; if(sp_is_quit()) break; continue; } /* Set timeouts on client */ if(setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &(g_state.timeout), sizeof(g_state.timeout)) < 0 || setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &(g_state.timeout), sizeof(g_state.timeout)) < 0) sp_message(NULL, LOG_DEBUG, "couldn't set timeouts on incoming connection"); fcntl(fd, F_SETFD, fcntl(fd, F_GETFD, 0) | FD_CLOEXEC); /* Look for thread and also clean up others */ for(i = 0; i < g_state.max_threads; i++) { /* Find a thread to run or clean up old threads */ if(threads[i].tid != 0) { sp_lock(); x = threads[i].fd; sp_unlock(); if(x == -1) { sp_messagex(NULL, LOG_DEBUG, "cleaning up completed thread"); pthread_join(threads[i].tid, NULL); threads[i].tid = 0; } #ifdef _DEBUG else { /* For debugging connection problems: */ sp_messagex(NULL, LOG_DEBUG, "active connection thread: %x", (int)threads[i].tid); } #endif } /* Start a new thread if neccessary */ if(fd != -1 && threads[i].tid == 0) { threads[i].fd = fd; r = pthread_create(&(threads[i].tid), NULL, thread_main, (void*)(threads + i)); if(r != 0) { errno = r; sp_message(NULL, LOG_ERR, "couldn't create thread"); write(fd, SMTP_STARTFAILED, KL(SMTP_STARTFAILED)); shutdown(fd, SHUT_RDWR); close(fd); fd = -1; break; } sp_messagex(NULL, LOG_DEBUG, "created thread for connection"); fd = -1; break; } } /* Check to make sure we have a thread */ if(fd != -1) { sp_messagex(NULL, LOG_ERR, "too many connections open (max %d). sent busy response", g_state.max_threads); write(fd, SMTP_STARTBUSY, KL(SMTP_STARTBUSY)); shutdown(fd, SHUT_RDWR); close(fd); fd = -1; } } sp_messagex(NULL, LOG_DEBUG, "waiting for threads to quit"); /* Quit all threads here */ for(i = 0; i < g_state.max_threads; i++) { /* Clean up quit threads */ if(threads[i].tid != 0) { if(threads[i].fd != -1) { sp_lock(); fd = threads[i].fd; threads[i].fd = -1; sp_unlock(); shutdown(fd, SHUT_RDWR); close(fd); } sp_messagex(NULL, LOG_DEBUG, "cleaning up completed thread"); pthread_join(threads[i].tid, NULL); threads[i].tid = 0; } } free(threads); } static spctx_t* init_thread(int fd) { spctx_t* ctx; ctx = cb_new_context(); if(ctx) { memset(ctx, 0, sizeof(*ctx)); spio_init(&(ctx->server), "SERVER"); spio_init(&(ctx->client), "CLIENT"); sp_lock(); /* Assign a unique id to the connection */ ctx->id = g_unique_id++; /* We don't care about wraps, but we don't want zero */ if(g_unique_id == 0) g_unique_id++; sp_unlock(); sp_messagex(ctx, LOG_DEBUG, "processing %d on thread %x", fd, (int)pthread_self()); /* Connect to the outgoing server ... */ if(make_connections(ctx, fd) == -1) { cb_del_context(ctx); ctx = NULL; } } return ctx; } static void cleanup_context(spctx_t* ctx) { ASSERT(ctx); if(ctx->cachefile) { fclose(ctx->cachefile); ctx->cachefile = NULL; } if(ctx->cachename[0]) { unlink(ctx->cachename); ctx->cachename[0] = 0; } if(ctx->recipients) { free(ctx->recipients); ctx->recipients = NULL; } if(ctx->sender) { free(ctx->sender); ctx->sender = NULL; } if(ctx->xforwardaddr) { free(ctx->xforwardaddr); ctx->xforwardaddr = NULL; } if(ctx->xforwardhelo) { free(ctx->xforwardhelo); ctx->xforwardhelo = NULL; } ctx->logline[0] = 0; sp_add_log(ctx, "client=", ctx->client.peername); } static void done_thread(spctx_t* ctx) { ASSERT(ctx); spio_disconnect(ctx, &(ctx->client)); spio_disconnect(ctx, &(ctx->server)); /* Clean up file stuff */ cleanup_context(ctx); cb_del_context(ctx); } static void* thread_main(void* arg) { spthread_t* thread = (spthread_t*)arg; spctx_t* ctx = NULL; int processing = 0; int ret = 0; int fd; ASSERT(thread); siginterrupt(SIGINT, 1); siginterrupt(SIGTERM, 1); sp_lock(); /* Get the client socket */ fd = thread->fd; sp_unlock(); /* Sometimes we get to this point and then quit is noted */ if(sp_is_quit() || (ctx = init_thread(fd)) == NULL) { /* Special case. We don't have a context so clean up descriptor */ close(fd); /* new_context() should have already logged reason */ RETURN(-1); } /* call the processor */ processing = 1; ret = smtp_passthru(ctx); cleanup: if(ctx) { /* Let the client know about fatal errors */ if(!processing && ret == -1 && spio_valid(&(ctx->client))) spio_write_data(ctx, &(ctx->client), SMTP_STARTFAILED); done_thread(ctx); } /* mark this as done */ sp_lock(); thread->fd = -1; sp_unlock(); return (void*)(ret == 0 ? (size_t)0 : (size_t)1); } static int make_connections(spctx_t* ctx, int client) { struct sockaddr_any peeraddr; struct sockaddr_any peersrc; struct sockaddr_any addr; struct sockaddr_any* dstaddr; struct sockaddr_any* srcaddr; char buf[MAXPATHLEN]; const char* dstname; const char* srcname; #ifdef USE_PF_NATLOOKUP struct pfioc_natlook nl; int dev; #endif /* USE_PF_NATLOOKUP */ ASSERT(client != -1); /* Setup the incoming connection. This also fills in peeraddr for us */ spio_attach(ctx, &(ctx->client), client, &peeraddr); sp_messagex(ctx, LOG_INFO, "accepted connection from: %s", ctx->client.peername); /* Create the server connection address */ dstaddr = &(g_state.outaddr); dstname = g_state.outname; srcaddr = NULL; srcname = NULL; /* For transparent proxying we have to discover the address to connect to */ if(g_state.transparent != TRANSPARENT_OFF) { memset(&addr, 0, sizeof(addr)); SANY_LEN(addr) = sizeof(addr); #ifdef LINUX_NETFILTER if(getsockopt(ctx->client.fd, SOL_IP, SO_ORIGINAL_DST, &SANY_ADDR(addr), &SANY_LEN(addr)) == -1) #else if(getsockname(ctx->client.fd, &SANY_ADDR(addr), &SANY_LEN(addr)) == -1) #endif { sp_message(ctx, LOG_ERR, "couldn't get source address for transparent proxying"); return -1; } #ifdef USE_PF_NATLOOKUP dev = open("/dev/pf", O_RDWR); if (dev == -1) { sp_message(ctx, LOG_ERR, "open(\"/dev/pf\") failed"); return -1; } memset(&nl, 0, sizeof(struct pfioc_natlook)); memcpy(&nl.saddr.v4.s_addr, &(peeraddr.s.in.sin_addr), sizeof(struct in_addr)); nl.sport = peeraddr.s.in.sin_port; memcpy(&nl.daddr.v4.s_addr, &(addr.s.in.sin_addr), sizeof(struct in_addr)); nl.dport = addr.s.in.sin_port; nl.af = AF_INET; nl.proto = IPPROTO_TCP; if (ioctl(dev, DIOCNATLOOK, &nl)) { sp_message(ctx, LOG_ERR, "DIOCNATLOOK"); return -1; } if (close(dev) != 0) { sp_message(ctx, LOG_ERR, "close(\"/dev/pf\") failed"); return -1; } memcpy(&(addr.s.in.sin_addr), &nl.rdaddr.v4.s_addr, sizeof(struct in_addr)); addr.s.in.sin_port = nl.rdport; addr.s.in.sin_family = AF_INET; #endif /* USE_PF_NATLOOKUP */ /* Check address types */ if(sock_any_cmp(&addr, &peeraddr, SANY_OPT_NOPORT) == 0) { sp_messagex(ctx, LOG_ERR, "loop detected in transparent proxying"); return -1; } dstaddr = &addr; } if (g_state.transparent == TRANSPARENT_FULL) { #ifdef HAVE_IP_TRANSPARENT sock_any_cpy (&peersrc, &peeraddr, SANY_OPT_NOPORT); srcaddr = &peersrc; srcname = ctx->client.peername; #endif } /* Check for loopback option */ else if(SANY_TYPE(*dstaddr) == AF_INET && dstaddr->s.in.sin_addr.s_addr == 0) { /* Use the incoming IP as the default */ memcpy(&addr, &(g_state.outaddr), sizeof(addr)); memcpy(&(addr.s.in.sin_addr), &(peeraddr.s.in.sin_addr), sizeof(addr.s.in.sin_addr)); dstaddr = &addr; } #ifdef HAVE_INET6 /* IPv6 loopback? */ else if(SANY_TYPE(*outaddr) == AF_INET6 && outaddr->s.in.in6.sin_addr.s_addr == 0) { /* Use the incoming IP as the default */ memcpy(&addr, &(g_state.outaddr), sizeof(addr)); memcpy(&(addr.s.in.sin6_addr), &(peeraddr.s.in.sin6_addr), sizeof(addr.s.in.sin6_addr)); outaddr = &addr; } #endif /* Not transparent proxy or loopback */ else { /* Resolve any DNS name again */ if(sock_any_pton(g_state.outname, &addr, SANY_OPT_DEFPORT(25)) != -1) memcpy(&(g_state.outaddr), &addr, sizeof(g_state.outaddr)); else sp_messagex(ctx, LOG_WARNING, "couldn't resolve " CFG_OUTADDR ": %s", g_state.outname); } /* Reparse name if needed */ if(dstaddr != &(g_state.outaddr)) { if(sock_any_ntop(dstaddr, buf, MAXPATHLEN, 0) != -1) dstname = buf; else dstname = "unknown"; } /* Connect to the server */ if(spio_connect(ctx, &(ctx->server), dstaddr, dstname, srcaddr, srcname) == -1) return -1; return 0; } /* ---------------------------------------------------------------------------------- * SMTP HANDLING */ static int should_skip_processing(spctx_t* ctx) { if(ctx->authenticated && (g_state.skip & SKIP_AUTHENTICATED)) { sp_messagex(ctx, LOG_DEBUG, "skipping processing because client is authenticated"); return 1; } return 0; } static int smtp_passthru(spctx_t* ctx) { char* t; const char* p; int r, cont, ret = 0; unsigned int mask; int neterror = 0; int first_rsp = 1; /* The first 220 response from server to be filtered */ int filter_host = 0; /* Next response is 250 hostname, which we change */ int auth_started = 0; /* Started performing authentication */ /* XCLIENT is for use in access control */ int xclient_sup = 0; /* Is XCLIENT supported? */ int xclient_sent = 0; /* Have we sent an XCLIENT command? */ ASSERT(spio_valid(&(ctx->client)) && spio_valid(&(ctx->server))); #define C_LINE ctx->client.line #define S_LINE ctx->server.line while(!sp_is_quit()) { mask = spio_select(ctx, &(ctx->client), &(ctx->server), NULL); if(mask == ~0) { neterror = 1; RETURN(-1); } /* Client has data available, read a line and process */ if(mask & 1) { if((r = spio_read_line(ctx, &(ctx->client), SPIO_DISCARD)) == -1) RETURN(-1); /* Client disconnected, we're done */ if(r == 0) RETURN(0); /* We don't let clients send really long lines */ if(LINE_TOO_LONG(r)) { if(spio_write_data(ctx, &(ctx->client), SMTP_TOOLONG) == -1) RETURN(-1); continue; } /* Only valid after EHLO or HELO commands */ filter_host = 0; /* * At this point we may want to send our XCLIENT. This is a per * connection command. */ if(xclient_sup && !xclient_sent && g_state.xclient) { sp_messagex(ctx, LOG_DEBUG, "sending XCLIENT"); if(spio_write_dataf(ctx, &(ctx->server), SMTP_XCLIENT, ctx->client.peername) == -1) RETURN(-1); if(read_server_response(ctx) == -1) RETURN(-1); if(!get_successful_rsp(S_LINE, NULL)) sp_messagex(ctx, LOG_WARNING, "server didn't accept XCLIENT"); xclient_sent = 1; } /* Handle the DATA section via our AV checker */ if(is_first_word(C_LINE, DATA_CMD, KL(DATA_CMD))) { if(should_skip_processing (ctx)) { if(sp_pass_data(ctx) < 0) RETURN(-1); } else { /* * Now go into scan mode. This also handles the eventual * sending of the data to the server, making the av check * transparent */ if(cb_check_data(ctx) == -1) RETURN(-1); } /* Print the log out for this email */ sp_messagex(ctx, LOG_INFO, "%s", ctx->logline); /* Done with that email */ cleanup_context(ctx); /* Command handled */ continue; } /* * We need our response to HELO and EHLO to be modified in order * to prevent complaints about mail loops */ else if(is_first_word(C_LINE, EHLO_CMD, KL(EHLO_CMD))) { /* EHLO can have multline responses so we set a flag */ filter_host = 1; } /* * We always support XCLIENT on a HELO type connection. We do this * for security reasons, so that a client can't get around filtering * by backing up one on the protocol. */ else if(is_first_word(C_LINE, HELO_CMD, KL(HELO_CMD))) { sp_messagex(ctx, LOG_DEBUG, "XCLIENT support assumed"); xclient_sup = 1; /* Filter host as with EHLO above */ filter_host = 1; } /* * We don't like these commands. Filter them out. We should have * filtered out their service extensions earlier in the EHLO response. * This is just for errant clients. */ else if(is_first_word(C_LINE, STARTTLS_CMD, KL(STARTTLS_CMD)) || is_first_word(C_LINE, BDAT_CMD, KL(BDAT_CMD))) { sp_messagex(ctx, LOG_DEBUG, "ESMTP feature not supported"); if(spio_write_data(ctx, &(ctx->client), SMTP_NOTSUPP) == -1) RETURN(-1); /* Command handled */ continue; } /* * For security reasons we're not about to forward any XCLIENTs * from our client through. This could lead to a client using our * privileged IP address to change an audit trail or relay etc... */ else if(is_first_word(C_LINE, XCLIENT_CMD, KL(XCLIENT_CMD))) { sp_messagex(ctx, LOG_WARNING, "client attempted use of privileged XCLIENT feature"); if(spio_write_data(ctx, &(ctx->client), SMTP_NOTAUTH) == -1) RETURN(-1); /* Command handled */ continue; } else if(is_first_word(C_LINE, AUTH_CMD, KL(AUTH_CMD))) { auth_started = 1; } else if(check_first_word(C_LINE, FROM_CMD, KL(FROM_CMD), SMTP_DELIMS) > 0 || check_first_word(C_LINE, TO_CMD, KL(TO_CMD), SMTP_DELIMS) > 0) { if(!should_skip_processing(ctx)) { r = cb_check_pre(ctx); if(r < 0) { RETURN(-1); } else if(r == 0) { cleanup_context(ctx); continue; } } } /* All other commands just get passed through to server */ if(spio_write_data(ctx, &(ctx->server), C_LINE) == -1) RETURN(-1); continue; } /* Server has data available, read a line and forward */ if(mask & 2) { if((r = spio_read_line(ctx, &(ctx->server), SPIO_DISCARD)) == -1) RETURN(-1); if(r == 0) RETURN(0); if(LINE_TOO_LONG(r)) sp_messagex(ctx, LOG_WARNING, "SMTP response line too long. discarded extra"); /* * We intercept the first response we get from the server. * This allows us to change header so that it doesn't look * to the client server that we're in a wierd loop. * * In different situations using the local hostname or * 'localhost' don't work because the receiving mail server * expects one of those to be its own name. We use 'clamsmtp' * instead. No properly configured server would have this * as their domain name, and RFC 2821 allows us to use * an arbitrary but identifying string. */ if(first_rsp) { first_rsp = 0; if(is_first_word(S_LINE, START_RSP, KL(START_RSP))) { sp_messagex(ctx, LOG_DEBUG, "intercepting initial response"); if(spio_write_data(ctx, &(ctx->client), SMTP_BANNER) == -1) RETURN(-1); /* Command handled */ continue; } } if((p = get_successful_rsp(S_LINE, &cont)) != NULL) { /* * Certain mail servers (Postfix 1.x in particular) do a loop check * on the 250 response after a EHLO or HELO. This is where we * filter that to prevent loopback errors. */ if(filter_host) { /* Can have multi-line responses, and we want to be * sure to only replace the first one. */ filter_host = 0; sp_messagex(ctx, LOG_DEBUG, "intercepting host response"); if(spio_write_data(ctx, &(ctx->client), cont ? SMTP_EHLO_RSP : SMTP_HELO_RSP) == -1) RETURN(-1); /* A new email so cleanup */ cleanup_context(ctx); continue; } /* * Filter out any EHLO responses that we can't or don't want * to support. For example pipelining or TLS. */ if(is_first_word(C_LINE, EHLO_CMD, KL(EHLO_CMD))) { /* * On ESMTP connections we let the server tell us whether it * wants XCLIENTs or not. (In contrast to old SMTP above). */ if(is_first_word(p, ESMTP_XCLIENT, KL(ESMTP_XCLIENT))) { sp_messagex(ctx, LOG_DEBUG, "XCLIENT supported"); xclient_sup = 1; } if(is_first_word(p, ESMTP_PIPELINE, KL(ESMTP_PIPELINE)) || is_first_word(p, ESMTP_TLS, KL(ESMTP_TLS)) || is_first_word(p, ESMTP_CHUNK, KL(ESMTP_CHUNK)) || is_first_word(p, ESMTP_BINARY, KL(ESMTP_BINARY)) || is_first_word(p, ESMTP_CHECK, KL(ESMTP_CHECK)) || is_first_word(p, ESMTP_XCLIENT, KL(ESMTP_XCLIENT)) || is_first_word(p, ESMTP_XEXCH50, KL(ESMTP_XEXCH50))) { sp_messagex(ctx, LOG_DEBUG, "filtered ESMTP feature: %s", trim_space((char*)p)); /* * If this is the last line in the EHLO response we need * to replace it with something else */ if(!cont) { if(spio_write_data(ctx, &(ctx->client), SMTP_FEAT_RSP) == -1) RETURN(-1); } continue; } } /* MAIL FROM (that the server accepted) */ if((r = check_first_word(C_LINE, FROM_CMD, KL(FROM_CMD), SMTP_DELIMS)) > 0) { t = parse_address(C_LINE + r); sp_add_log(ctx, "from=", t); /* Make note of the sender for later */ ctx->sender = (char*)reallocf(ctx->sender, strlen(t) + 1); if(ctx->sender) strcpy(ctx->sender, t); } /* RCPT TO (that the server accepted) */ else if((r = check_first_word(C_LINE, TO_CMD, KL(TO_CMD), SMTP_DELIMS)) > 0) { t = parse_address(C_LINE + r); sp_add_log(ctx, "to=", t); /* Make note of the recipient for later */ r = ctx->recipients ? strlen(ctx->recipients) : 0; ctx->recipients = (char*)reallocf(ctx->recipients, r + strlen(t) + 2); if(ctx->recipients) { /* Recipients are separated by lines */ if(r != 0) strcat(ctx->recipients, "\n"); else ctx->recipients[0] = 0; strcat(ctx->recipients, t); } } /* * If the client sends an XFORWARD, and the server accepted it, * we store address for use in our forked process environment * variables (see sp_setup_forked). */ else if(is_first_word(C_LINE, XFORWARD_CMD, KL(XFORWARD_CMD))) { if((t = parse_xforward (C_LINE + KL(XFORWARD_CMD), "ADDR"))) { ctx->xforwardaddr = (char*)reallocf(ctx->xforwardaddr, strlen(t) + 1); if(ctx->xforwardaddr) strcpy(ctx->xforwardaddr, t); } if((t = parse_xforward (C_LINE + KL(XFORWARD_CMD), "HELO"))) { ctx->xforwardhelo = (char*)reallocf(ctx->xforwardhelo, strlen(t) + 1); if(ctx->xforwardhelo) strcpy(ctx->xforwardhelo, t); } } /* RSET */ else if(is_first_word(C_LINE, RSET_CMD, KL(RSET_CMD))) { cleanup_context(ctx); } /* Successful authentication */ else if(is_first_word(S_LINE, AUTH_SUCCESS_RSP, KL(AUTH_SUCCESS_RSP))) { if(auth_started) { sp_messagex(ctx, LOG_DEBUG, "Client authenticated successfully"); ctx->authenticated = 1; } else { sp_messagex(ctx, LOG_WARNING, "Authentication success code without AUTH"); } } } /* Send the line to the client */ if(spio_write_data(ctx, &(ctx->client), S_LINE) == -1) RETURN(-1); continue; } } cleanup: if(!neterror && ret == -1 && spio_valid(&(ctx->client))) spio_write_data(ctx, &(ctx->client), SMTP_FAILED); return ret; } /* ----------------------------------------------------------------------------- * SMTP PASSTHRU FUNCTIONS FOR DATA CHECK */ static char* parse_address(char* line) { char* t; line = trim_start(line); /* * We parse out emails in the form of * as well as accept other addresses. */ if(strncmp(line, "<>", 2) == 0) return("<>"); if(line[0] == '<') { if((t = strchr(line, '>')) != NULL) { *t = 0; line++; return line; } } return trim_end(line); } static char* parse_xforward(char* line, const char* part) { char* t; char* e; t = strcasestr(line, part); if(!t) return NULL; /* equals sign and surrounding */ t = trim_start(t + strlen(part)); if(*t != '=') return NULL; t = trim_start(t + 1); if(!*t) return NULL; /* Find the end of the thingy */ if(*t == '[') { t++; e = strchr(t, ']'); } else { e = t + strcspn(t, " \t"); } if(!e) return NULL; *e = 0; return t; } static const char* get_successful_rsp(const char* line, int* cont) { /* * We check for both '250 xxx' type replies * and the continued response '250-xxxx' type */ line = trim_start(line); if(line[0] == '2' && isdigit(line[1]) && isdigit(line[2]) && (line[3] == ' ' || line[3] == '-')) { if(cont) *cont = (line[3] == '-'); return line + 4; } return NULL; } void sp_add_log(spctx_t* ctx, char* prefix, char* line) { char* t = ctx->logline; int l = strlen(t); int x; ASSERT(l <= SP_LOG_LINE_LEN); /* Add up necessary lengths */ x = 2 + strlen(prefix) + strlen(line) + 1; if(l + x >= SP_LOG_LINE_LEN) l = SP_LOG_LINE_LEN - x; t += l; l = SP_LOG_LINE_LEN - l; *t = 0; if(ctx->logline[0] != 0) strlcat(t, ", ", l); strlcat(t, prefix, l); /* Skip initial white space */ line = trim_start(line); strlcat(t, line, l); /* Skip later white space */ trim_end(t); } int sp_start_data(spctx_t* ctx) { /* Send back the intermediate response to the client */ return spio_write_data(ctx, &(ctx->client), SMTP_DATAINTERMED); } int sp_read_data(spctx_t* ctx, const char** data) { int r; ASSERT(ctx); ASSERT(data); *data = NULL; switch(r = spio_read_line(ctx, &(ctx->client), SPIO_QUIET)) { case 0: sp_messagex(ctx, LOG_ERR, "unexpected end of data from client"); return -1; case -1: /* Message already printed */ return -1; }; if(g_state.keepalives > 0) { /* * During this time we're just reading from the client. If we haven't * had any interaction with the server recently then send something * to let it know we're still around. */ if((ctx->server.last_action + g_state.keepalives) < time(NULL)) do_server_noop(ctx); } if(ctx->_crlf && strcmp(ctx->client.line, DATA_END_SIG) == 0) return 0; /* Check if this line ended with a CRLF */ ctx->_crlf = (strcmp(CRLF, ctx->client.line + (r - KL(CRLF))) == 0); *data = ctx->client.line; return r; } int sp_write_data(spctx_t* ctx, const char* buf, int len) { int r = 0; ASSERT(ctx); /* When a null buffer close the cache file */ if(!buf) { if(ctx->cachefile) { if(fclose(ctx->cachefile) == EOF) { sp_message(ctx, LOG_ERR, "couldn't write to cache file: %s", ctx->cachename); r = -1; } ctx->cachefile = NULL; } return r; } /* Make sure we have a file open */ if(!ctx->cachefile) { int tfd; /* Make sure afore mentioned file is gone */ if(ctx->cachename[0]) unlink(ctx->cachename); snprintf(ctx->cachename, MAXPATHLEN, "%s/%s.XXXXXX", g_state.directory, g_state.name); if((tfd = mkstemp(ctx->cachename)) == -1 || (ctx->cachefile = fdopen(tfd, "w")) == NULL) { if(tfd != -1) close(tfd); sp_message(ctx, LOG_ERR, "couldn't open cache file"); return -1; } fcntl(tfd, F_SETFD, fcntl(tfd, F_GETFD, 0) | FD_CLOEXEC); sp_messagex(ctx, LOG_DEBUG, "created cache file: %s", ctx->cachename); } fwrite(buf, 1, len, ctx->cachefile); if(ferror(ctx->cachefile)) { sp_message(ctx, LOG_ERR, "couldn't write to cache file: %s", ctx->cachename); return -1; } return len; } int sp_cache_data(spctx_t* ctx) { int r, count = 0; const char* data; while((r = sp_read_data(ctx, &data)) != 0) { if(r < 0) return -1; /* Message already printed */ count += r; if((r = sp_write_data(ctx, data, r)) < 0) return -1; /* Message already printed */ } /* End the caching */ if(sp_write_data(ctx, NULL, 0) < 0) return -1; sp_messagex(ctx, LOG_DEBUG, "wrote %d bytes to cache", count); return count; } /* Important: |date| should be at least MAX_DATE_LENGTH long */ static void make_date(spctx_t* ctx, char* date) { size_t date_len; struct tm t2; time_t t; /* Get a basic date like: 'Wed Jun 30 21:49:08 1993' */ if(time(&t) == (time_t)-1 || !localtime_r(&t, &t2) || !asctime_r(&t2, date)) { sp_message(ctx, LOG_WARNING, "unable to get date for header"); date[0] = 0; return; } trim_end(date); date_len = strlen(date); { #ifdef HAVE_TM_GMTOFF time_t timezone = t2.tm_gmtoff; const char *tzname[2] = { t2.tm_zone, t2.tm_zone }; snprintf(date + date_len, MAX_DATE_LENGTH - date_len, " %+03d%02d (%s)", (int)(timezone / 3600), (int)(timezone % 3600), tzname[t2.tm_isdst ? 1 : 0]); #else /* Apparently Solaris needs this nasty hack.... */ #define DAY_MIN (24 * HOUR_MIN) #define HOUR_MIN 60 #define MIN_SEC 60 struct tm gmt; struct tm *lt; int off; gmt = *gmtime(&t); lt = localtime(&t); off = (lt->tm_hour - gmt.tm_hour) * HOUR_MIN + lt->tm_min - gmt.tm_min; if (lt->tm_year < gmt.tm_year) off -= DAY_MIN; else if (lt->tm_year > gmt.tm_year) off += DAY_MIN; else if (lt->tm_yday < gmt.tm_yday) off -= DAY_MIN; else if (lt->tm_yday > gmt.tm_yday) off += DAY_MIN; if (lt->tm_sec <= gmt.tm_sec - MIN_SEC) off -= 1; else if (lt->tm_sec >= gmt.tm_sec + MIN_SEC) off += 1; snprintf(date + date_len, MAX_DATE_LENGTH - date_len, " %+03d%02d (%s)", (int)(off / HOUR_MIN), (int)(abs(off) % HOUR_MIN), tzname[lt->tm_isdst ? 1 : 0]); #endif } /* Break it off just in case */ date[MAX_DATE_LENGTH - 1] = 0; } /* Important: |header| should be a buffer of MAX_HEADER_LENGTH */ static int make_header(spctx_t* ctx, const char* format_str, char* header) { char date[MAX_DATE_LENGTH]; int remaining, l; const char* f; char* p; date[0] = 0; remaining = MAX_HEADER_LENGTH - 1; p = header; /* Parse the format string and replace special characters with our data */ for(f = format_str; *f && remaining > 0; f++) { /* A backslash escapes certain characters */ if(f[0] == '\\' && f[1] != 0) { switch(*(++f)) { case 'r': *p = '\r'; break; case 'n': *p = '\n'; break; case 't': *p = '\t'; break; default: *p = *f; break; } ++p; --remaining; } /* * Special symbols: * %i: client's IP * %l: server's IP * %d: date */ else if(f[0] == '%' && f[1] != 0) { switch(*(++f)) { case 'i': l = strlen(ctx->client.peername); strncpy(p, ctx->client.peername, remaining); remaining -= l; p += l; break; case 'l': l = strlen(ctx->client.localname); strncpy(p, ctx->client.localname, remaining); remaining -= l; p += l; break; case 'd': if(date[0] == 0) make_date(ctx, date); l = strlen(date); strncpy(p, date, remaining); remaining -= l; p += l; break; case '%': *p = '%'; ++p; break; default: sp_messagex(ctx, LOG_WARNING, "invalid header symbol: %%%c", *f); break; }; } else { *(p++) = *f; remaining--; } } if((p + 1) < (header + MAX_HEADER_LENGTH)) p[1] = 0; header[MAX_HEADER_LENGTH - 1] = 0; l = p - header; return l >= MAX_HEADER_LENGTH ? MAX_HEADER_LENGTH - 1 : l; } int sp_done_data(spctx_t* ctx, const char *headertmpl) { FILE* file = 0; int ret = 0; char *line; char header[MAX_HEADER_LENGTH] = ""; size_t header_len, line_len; int header_prepend = 0; ssize_t rc; ASSERT(ctx->cachename[0]); /* Must still be around */ ASSERT(!ctx->cachefile); /* File must be closed */ memset(header, 0, sizeof(header)); /* Alloc line buffer */ line_len = SP_LINE_LENGTH; if((line = (char *)malloc(line_len)) == NULL) RETURN(-1); /* Open the file */ file = fopen(ctx->cachename, "r"); if(file == NULL) { sp_message(ctx, LOG_ERR, "couldn't open cache file: %s", ctx->cachename); RETURN(-1); } /* Ask the server for permission to send data */ if(spio_write_data(ctx, &(ctx->server), SMTP_DATA) == -1) RETURN(-1); if(read_server_response(ctx) == -1) RETURN(-1); /* If server returns an error then tell the client */ if(!is_first_word(ctx->server.line, DATA_RSP, KL(DATA_RSP))) { if(spio_write_data(ctx, &(ctx->client), ctx->server.line) == -1) RETURN(-1); sp_messagex(ctx, LOG_DEBUG, "server refused data transfer"); RETURN(0); } sp_messagex(ctx, LOG_DEBUG, "sending from cache file: %s", ctx->cachename); if(headertmpl) { header_len = make_header(ctx, headertmpl, header); if(is_first_word(RCVD_HEADER, header, KL(RCVD_HEADER))) header_prepend = 1; } /* If we have to prepend the header, do it */ if(header[0] != '\0' && header_prepend) { if(spio_write_data_raw(ctx, &(ctx->server), (unsigned char*)header, header_len) == -1 || spio_write_data_raw(ctx, &(ctx->server), (unsigned char*)CRLF, KL(CRLF)) == -1) RETURN(-1); header[0] = '\0'; } /* Transfer actual file data */ while((rc = getline(&line, &line_len, file)) != -1) { /* * If the line is . we need to change it so that * it doesn't end the email. We do this by adding a space. * This won't occur much in clamsmtpd, but proxsmtpd might * have filters that accidentally put this in. */ if(strcmp(line, "." CRLF) == 0) strncpy(line, ". " CRLF, SP_LINE_LENGTH); if(header[0] != '\0') { /* * The first blank line we see means the headers are done. * At this point we add in our virus checked header. */ if(is_blank_line(line)) { if(spio_write_data_raw(ctx, &(ctx->server), (unsigned char*)header, header_len) == -1 || spio_write_data_raw(ctx, &(ctx->server), (unsigned char*)CRLF, KL(CRLF)) == -1) RETURN(-1); header[0] = '\0'; } } if(spio_write_data_raw(ctx, &(ctx->server), (unsigned char*)line, rc) == -1) RETURN(-1); } if(ferror(file)) sp_message(ctx, LOG_ERR, "error reading cache file: %s", ctx->cachename); if(ferror(file) || spio_write_data(ctx, &(ctx->server), DATA_END_SIG) == -1) { /* Tell the client it went wrong */ spio_write_data(ctx, &(ctx->client), SMTP_FAILED); RETURN(-1); } sp_messagex(ctx, LOG_DEBUG, "sent email data"); /* Okay read the response from the server and echo it to the client */ if(read_server_response(ctx) == -1) RETURN(-1); if(spio_write_data(ctx, &(ctx->client), ctx->server.line) == -1) RETURN(-1); cleanup: if(line) free(line); if(file) fclose(file); /* read-only so no error check */ return ret; } int sp_pass_data(spctx_t* ctx) { int count = 0; const char *data; ssize_t rc; /* Ask the server for permission to send data */ if(spio_write_data(ctx, &(ctx->server), SMTP_DATA) < 0) return -1; if(read_server_response(ctx) == -1) return -1; /* Send the response to the client */ if(spio_write_data(ctx, &(ctx->client), ctx->server.line) == -1) return -1; /* If server returns an error then tell the client */ if(!is_first_word(ctx->server.line, DATA_RSP, KL(DATA_RSP))) { sp_messagex(ctx, LOG_DEBUG, "server refused data transfer"); return 0; } while((rc = sp_read_data(ctx, &data)) != 0) { if(rc < 0) return -1; /* Message already printed */ count += rc; if(spio_write_data_raw(ctx, &(ctx->server), (unsigned char*)data, rc) < 0) return -1; } if(spio_write_data(ctx, &(ctx->server), DATA_END_SIG) < 0) { /* Tell the client it went wrong */ spio_write_data(ctx, &(ctx->client), SMTP_FAILED); return -1; } /* Okay read the response from the server and echo it to the client */ if(read_server_response(ctx) == -1) return -1; if(spio_write_data(ctx, &(ctx->client), ctx->server.line) == -1) return -1; sp_add_log(ctx, "status=", "SKIPPED"); sp_messagex(ctx, LOG_DEBUG, "skipped %d data bytes", count); return 0; } int sp_fail_data(spctx_t* ctx, const char* smtp_status) { if(sp_fail_msg(ctx, smtp_status) < 0) return -1; /* Tell the server to forget about the current message */ if(spio_write_data(ctx, &(ctx->server), SMTP_RSET) == -1 || read_server_response(ctx) == -1) return -1; return 0; } int sp_fail_msg(spctx_t* ctx, const char* smtp_status) { char buf[256 + KL(SMTP_REJPREFIX) + KL(CRLF) + 1]; char* t = NULL; int len, x; int pref = 0; int crlf = 0; if(smtp_status == NULL) smtp_status = SMTP_FAILED; x = strtol(smtp_status, &t, 10); len = strlen(smtp_status); /* We need 3 digits and CRLF at the end for a premade SMTP message */ if(x == 0 || t != smtp_status + 3) pref = 1; /* We need a CRLF at the end */ if(strcmp(smtp_status + (len - KL(CRLF)), CRLF) != 0) crlf = 1; if(pref || crlf) { /* Note that we truncate long lines */ snprintf(buf, sizeof(buf), "%s%.256s%s", pref ? SMTP_REJPREFIX : "", smtp_status, crlf ? CRLF : ""); buf[sizeof(buf) - 1] = 0; smtp_status = buf; } if(spio_write_data(ctx, &(ctx->client), smtp_status) == -1) return -1; return 0; } static int read_server_response(spctx_t* ctx) { int r; /* Read response line from the server */ if((r = spio_read_line(ctx, &(ctx->server), SPIO_DISCARD)) == -1) return -1; if(r == 0) { sp_messagex(ctx, LOG_ERR, "server disconnected unexpectedly"); /* Tell the client it went wrong */ spio_write_data(ctx, &(ctx->client), SMTP_FAILED); return 0; } if(LINE_TOO_LONG(r)) sp_messagex(ctx, LOG_WARNING, "SMTP response line too long. discarded extra"); return 0; } static void do_server_noop(spctx_t* ctx) { if(spio_valid(&(ctx->server))) { if(spio_write_data(ctx, &(ctx->server), SMTP_NOOP) != -1) spio_read_line(ctx, &(ctx->server), SPIO_DISCARD); } } void sp_setup_forked(spctx_t* ctx, int file) { /* Signals we've messed with */ signal(SIGPIPE, SIG_DFL); signal(SIGHUP, SIG_DFL); signal(SIGINT, SIG_DFL); signal(SIGTERM, SIG_DFL); siginterrupt(SIGINT, 0); siginterrupt(SIGTERM, 0); if(ctx->sender) setenv("SENDER", ctx->sender, 1); if(ctx->recipients) setenv("RECIPIENTS", ctx->recipients, 1); if(file && ctx->cachename[0]) setenv("EMAIL", ctx->cachename, 1); if(spio_valid(&(ctx->client))) setenv("CLIENT", ctx->client.peername, 1); if(ctx->xforwardaddr) setenv("REMOTE", ctx->xforwardaddr, 1); if(ctx->xforwardhelo) setenv("REMOTE_HELO", ctx->xforwardhelo, 1); if(spio_valid(&(ctx->server))) setenv("SERVER", ctx->server.peername, 1); setenv("TMPDIR", g_state.directory, 1); } /* ---------------------------------------------------------------------------------- * LOGGING */ const char kMsgDelimiter[] = ": "; #define MAX_MSGLEN 1024 static void vmessage(spctx_t* ctx, int level, int err, const char* msg, va_list ap) { char buf[MAX_MSGLEN]; int e = errno; if(g_state.daemonized) { if(level >= LOG_DEBUG) return; } else { if(g_state.debug_level < level) return; } ASSERT(msg); if(ctx) snprintf(buf, MAX_MSGLEN, "%06X: %s%s", ctx->id, msg, err ? ": " : ""); else snprintf(buf, MAX_MSGLEN, "%s%s", msg, err ? ": " : ""); if(err) { /* strerror_r doesn't want to work for us for some reason len = strlen(buf); strerror_r(e, buf + len, MAX_MSGLEN - len); */ sp_lock(); strncat(buf, strerror(e), MAX_MSGLEN); sp_unlock(); } /* As a precaution */ buf[MAX_MSGLEN - 1] = 0; /* Either to syslog or stderr */ if(g_state.daemonized) vsyslog(level, buf, ap); else vwarnx(buf, ap); } void sp_messagex(spctx_t* ctx, int level, const char* msg, ...) { va_list ap; va_start(ap, msg); vmessage(ctx, level, 0, msg, ap); va_end(ap); } void sp_message(spctx_t* ctx, int level, const char* msg, ...) { va_list ap; va_start(ap, msg); vmessage(ctx, level, 1, msg, ap); va_end(ap); } /* ----------------------------------------------------------------------- * LOCKING */ void sp_lock() { int r; #ifdef _DEBUG int wait = 0; #endif #ifdef _DEBUG r = pthread_mutex_trylock(&g_mutex); if(r == EBUSY) { wait = 1; sp_message(NULL, LOG_DEBUG, "thread will block: %d", pthread_self()); r = pthread_mutex_lock(&g_mutex); } #else r = pthread_mutex_lock(&g_mutex); #endif if(r != 0) { errno = r; sp_message(NULL, LOG_CRIT, "threading problem. couldn't lock mutex"); } #ifdef _DEBUG else if(wait) { sp_message(NULL, LOG_DEBUG, "thread unblocked: %d", pthread_self()); } #endif } void sp_unlock() { int r = pthread_mutex_unlock(&g_mutex); if(r != 0) { errno = r; sp_message(NULL, LOG_CRIT, "threading problem. couldn't unlock mutex"); } } /* ----------------------------------------------------------------------------- * CONFIG FILE */ int sp_parse_option(const char* name, const char* value) { char* t; int ret = 0; if(strcasecmp(CFG_MAXTHREADS, name) == 0) { g_state.max_threads = strtol(value, &t, 10); if(*t || g_state.max_threads <= 1 || g_state.max_threads >= TOP_MAX_CONNECTIONS) errx(2, "invalid setting: " CFG_MAXTHREADS " (must be between 1 and %d)", TOP_MAX_CONNECTIONS); ret = 1; } else if(strcasecmp(CFG_TIMEOUT, name) == 0) { g_state.timeout.tv_sec = strtol(value, &t, 10); if(*t || g_state.timeout.tv_sec <= 0) errx(2, "invalid setting: " CFG_TIMEOUT); ret = 1; } else if(strcasecmp(CFG_KEEPALIVES, name) == 0) { g_state.keepalives = strtol(value, &t, 10); if(*t || g_state.keepalives < 0) errx(2, "invalid setting: " CFG_KEEPALIVES); ret = 1; } else if(strcasecmp(CFG_XCLIENT, name) == 0) { if((g_state.xclient = strtob(value)) == -1) errx(2, "invalid value for " CFG_XCLIENT); ret = 1; } else if(strcasecmp(CFG_OUTADDR, name) == 0) { if(sock_any_pton(value, &(g_state.outaddr), SANY_OPT_DEFPORT(25)) == -1) errx(2, "invalid " CFG_OUTADDR " socket name or ip: %s", value); g_state.outname = value; ret = 1; } else if(strcasecmp(CFG_LISTENADDR, name) == 0) { if(sock_any_pton(value, &(g_state.listenaddr), SANY_OPT_DEFANY | SANY_OPT_DEFPORT(DEFAULT_PORT)) == -1) errx(2, "invalid " CFG_LISTENADDR " socket name or ip: %s", value); g_state.listenname = value; ret = 1; } else if(strcasecmp(CFG_TRANSPARENT, name) == 0) { int val = strtob(value); if(val == -1) { if(strcasecmp(VAL_CLIENT, value) == 0) { g_state.transparent = TRANSPARENT_CLIENT; } else if(strcasecmp(VAL_FULL, value) == 0) { #ifdef HAVE_IP_TRANSPARENT g_state.transparent = TRANSPARENT_FULL; #else errx(2, "invalid value for " CFG_TRANSPARENT ": was not built with support for true full transparent proxy"); #endif } else { errx(2, "invalid value for " CFG_TRANSPARENT); } } else if (val) { g_state.transparent = TRANSPARENT_CLIENT; } else { g_state.transparent = TRANSPARENT_OFF; } ret = 1; } else if(strcasecmp(CFG_DIRECTORY, name) == 0) { if(strlen(value) == 0) errx(2, "invalid setting: " CFG_DIRECTORY); g_state.directory = value; ret = 1; } else if(strcasecmp(CFG_USER, name) == 0) { if(strlen(value) == 0) errx(2, "invalid setting: " CFG_USER); g_state.user = value; ret = 1; } else if(strcasecmp(CFG_PIDFILE, name) == 0) { if(g_state.pidfile != NULL) sp_messagex(NULL, LOG_WARNING, "ignoring pid file specified on the command line. "); if(strlen(value) == 0) g_state.pidfile = NULL; else g_state.pidfile = value; ret = 1; } else if(strcasecmp(CFG_SKIP, name) == 0) { if(strcasecmp(value, VAL_AUTHENTICATED) == 0) g_state.skip |= SKIP_AUTHENTICATED; else errx(2, "invalid value for " CFG_SKIP " (must specify '" VAL_AUTHENTICATED "')"); return 1; } /* Always pass through to program */ if(cb_parse_option(name, value) == 1) ret = 1; return ret; } static int parse_config_file(const char* configfile) { FILE* f = NULL; long len; char* p; char* t; char* n; ASSERT(configfile); ASSERT(!g_state._p); f = fopen(configfile, "r"); if(f == NULL) { /* Soft errors when default config file and not found */ if((errno == ENOENT || errno == ENOTDIR)) return -1; else err(1, "couldn't open config file: %s", configfile); } /* Figure out size */ if(fseek(f, 0, SEEK_END) == -1 || (len = ftell(f)) == -1 || fseek(f, 0, SEEK_SET) == -1) err(1, "couldn't seek config file: %s", configfile); if((g_state._p = (char*)malloc(len + 2)) == NULL) errx(1, "out of memory"); /* And read in one block */ if(fread(g_state._p, 1, len, f) != len) err(1, "couldn't read config file: %s", configfile); fclose(f); sp_messagex(NULL, LOG_DEBUG, "read config file: %s", configfile); /* Double null terminate the data */ p = g_state._p; p[len] = '\n'; p[len + 1] = 0; n = g_state._p; /* Go through lines and process them */ while((t = strchr(n, '\n')) != NULL) { *t = 0; p = n; /* Do this before cleaning below */ n = t + 1; p = trim_start(p); /* Comments and empty lines */ if(*p == 0 || *p == '#') continue; /* Look for the break between name: value */ t = strchr(p, ':'); if(t == NULL) errx(2, "invalid config line: %s", p); /* Null terminate and split value part */ *t = 0; t++; t = trim_space(t); p = trim_space(p); /* Pass it through our options parsers */ if(sp_parse_option(p, t) == 0) /* If not recognized then it's invalid */ errx(2, "invalid config line: %s", p); sp_messagex(NULL, LOG_DEBUG, "parsed option: %s: %s", p, t); } return 0; }