2
Based on RCSid("$Id: amavis-milter.c,v 1.1.2.3.2.36 2003/03/07 17:07:19 lhecking Exp $")
3
* sendmail/milter client for amavis
6
* Author: Geoff Winkless <gwinkless@users.sourceforge.net>
7
* Additional work and patches by:
11
* Pierre-Yves Bonnetain
20
* Add some copyright notice here ...
26
/* Must be the same as the buffer length for recv() in amavisd */
27
#define SOCKBUFLEN 8192
30
#include <sys/types.h>
33
#include <sys/utsname.h>
41
#include <netinet/in.h>
42
#include <sys/socket.h>
50
#include "libmilter/mfapi.h"
52
#ifndef HAVE_SM_GEN_BOOL_TYPE
56
#define TEMPLATE "/amavis-milter-XXXXXXXX"
57
#define DEVNULL "/dev/null"
59
/* Extracted from the code for better configurability
60
* These will be set by configure/make eventually */
62
# define X_HEADER_TAG "X-Virus-Scanned"
65
# define X_HEADER_LINE "by amavisd-milter (http://www.amavis.org/)"
68
typedef struct llstrct {
74
struct in_addr client_addr;
88
#define DBG_ALL (DBG_FATAL | DBG_WARN | DBG_INFO)
90
/* Don't debug by default - use -d N option to switch it on */
91
static int debuglevel = DBG_NONE;
92
static const char *mydebugfile = RUNTIME_DIR "/amavis.client";
94
static int enable_x_header = 1;
96
static size_t mystrlcpy(char *, const char *, size_t);
97
static void mydebug(const int, const char *, ...);
98
static char *mymktempdir(char *);
99
static void freeenvto(ll *);
100
static sfsistat clearpriv(SMFICTX *, sfsistat, int);
101
static int allocmem(SMFICTX *);
102
static sfsistat mlfi_connect(SMFICTX *, char *, _SOCK_ADDR *);
103
static sfsistat mlfi_envto(SMFICTX *, char **);
104
static sfsistat mlfi_envfrom(SMFICTX *, char **);
105
static sfsistat mlfi_header(SMFICTX *, char *, char *);
106
static sfsistat mlfi_eoh(SMFICTX *);
107
static sfsistat mlfi_body(SMFICTX *, u_char *, size_t);
108
static sfsistat mlfi_eom(SMFICTX *);
109
static sfsistat mlfi_close(SMFICTX *);
110
static sfsistat mlfi_abort(SMFICTX *);
111
static sfsistat mlfi_cleanup(SMFICTX *, bool);
113
static struct utsname myuts;
116
mystrlcpy(char *dst, const char *src, size_t size)
118
size_t src_l = strlen(src);
120
memcpy(dst, src, src_l + 1);
122
memcpy(dst, src, size - 1);
123
dst[size - 1] = '\0';
129
mydebug(const int level, const char *fmt, ...)
137
/* Only bother to do something if we're told to with -d <n> */
138
if (!(level & debuglevel))
141
/* Set up debug file */
142
if (mydebugfile && (f = fopen(mydebugfile, "a")) == NULL) {
143
fprintf(stderr, "error opening '%s': %s\n", mydebugfile, strerror(errno));
148
timestamp = ctime(&tmpt);
149
/* A 26 character string according ctime(3c)
150
* we cut off the trailing \n\0 */
152
rv = fprintf(f, "%s %s amavis(client)[%ld]: ", timestamp,
153
(myuts.nodename ? myuts.nodename : "localhost"), (long) getpid());
155
perror("error writing (fprintf) to debug file");
158
rv = vfprintf(f, fmt, ap);
161
perror("error writing (vfprintf) to debug file");
165
perror("error writing (fputc) to debug file");
168
perror("error closing debug file f");
174
char dirname[BUFFLEN];
178
mystrlcpy(dirname, s, sizeof(dirname));
183
/* magic number alert */
184
while (count++ < 20) {
188
stt = strrchr(dirname, '-') + 1;
190
/* more magic number alert */
191
snprintf(stt, sizeof(dirname) - 1 - (stt - dirname), "%08d", lrand48() / 215);
192
/* This assumes that s in the calling function is the same size
193
* as the local dirname! Beware of malicious coders ;-) */
194
mystrlcpy(s, dirname, sizeof(dirname));
197
/* invalid template */
202
if (!mkdir(s, S_IRWXU)) {
210
#endif /* HAVE_MKDTEMP */
213
#define MLFIPRIV ((struct mlfiPriv *) smfi_getpriv(ctx))
216
freeenvto(ll * envto)
233
clearpriv(SMFICTX *ctx, sfsistat retme, int clearall)
235
/* release private memory and return retme */
236
struct mlfiPriv *priv = MLFIPRIV;
238
mydebug(DBG_INFO, "Clearing priv (clearall=%d)", clearall);
241
if (priv->mlfi_fname) {
242
mydebug(DBG_INFO, "clearing fname");
243
free(priv->mlfi_fname);
244
priv->mlfi_fname = NULL;
246
if (priv->mlfi_envfrom) {
247
mydebug(DBG_INFO, "clearing envfrom");
248
free(priv->mlfi_envfrom);
249
priv->mlfi_envfrom = NULL;
251
if (priv->mlfi_envto.next) {
252
mydebug(DBG_INFO, "clearing multi-envto");
253
freeenvto(priv->mlfi_envto.next);
254
priv->mlfi_envto.next = NULL;
256
if (priv->mlfi_envto.str) {
257
mydebug(DBG_INFO, "clearing envto");
258
free(priv->mlfi_envto.str);
259
priv->mlfi_envto.str = NULL;
262
mydebug(DBG_INFO, "clearing priv");
265
smfi_setpriv(ctx, priv);
271
* allocate some private memory if not already allocated
272
* returns 0 if ok, 1 if not
275
allocmem(SMFICTX * ctx)
277
struct mlfiPriv *priv = MLFIPRIV;
280
mydebug(DBG_INFO, "priv was null");
281
priv = malloc(sizeof *priv);
283
/* can't accept this message right now */
284
mydebug(DBG_FATAL, "failed to malloc %d bytes for private store: %s",
285
sizeof(*priv), strerror(errno));
288
mydebug(DBG_INFO, "malloced priv - now using memset()");
289
memset(priv, 0, sizeof *priv);
290
mydebug(DBG_INFO, "malloced priv successfully");
291
smfi_setpriv(ctx, priv);
293
mydebug(DBG_WARN, "allocmem tried but priv was already set");
294
mydebug(DBG_WARN, "priv->client_addr.s_addr is %d",
295
priv->client_addr.s_addr);
301
mlfi_connect(SMFICTX * ctx, char *hostname, _SOCK_ADDR * gen_hostaddr)
303
struct mlfiPriv *priv;
304
struct sockaddr_in *hostaddr;
306
hostaddr = (struct sockaddr_in *) gen_hostaddr;
309
mydebug(DBG_INFO, "hostname is %s, addr is %d.%d.%d.%d",
310
hostname, (hostaddr->sin_addr.s_addr) & 0xff,
311
(hostaddr->sin_addr.s_addr >> 8) & 0xff,
312
(hostaddr->sin_addr.s_addr >> 16) & 0xff,
313
(hostaddr->sin_addr.s_addr >> 24) & 0xff);
315
mydebug(DBG_INFO, "checking allocmem");
317
return SMFIS_TEMPFAIL;
320
priv->client_addr.s_addr = hostaddr->sin_addr.s_addr;
322
priv->client_addr.s_addr = 0;
324
smfi_setpriv(ctx, priv); /* not really needed but nice */
325
return SMFIS_CONTINUE;
329
mlfi_envto(SMFICTX * ctx, char **envto)
331
struct mlfiPriv *priv;
334
return SMFIS_TEMPFAIL;
337
if (!(priv->mlfi_thisenvto)) {
339
priv->mlfi_thisenvto = &(priv->mlfi_envto);
340
priv->mlfi_numto = 1;
343
if ((priv->mlfi_thisenvto->next = malloc(sizeof(ll))) == NULL)
344
return (SMFIS_TEMPFAIL);
345
priv->mlfi_thisenvto = priv->mlfi_thisenvto->next;
346
priv->mlfi_thisenvto->next = NULL;
348
if ((priv->mlfi_thisenvto->str = strdup(*envto)) == NULL)
349
return (SMFIS_TEMPFAIL);
350
mydebug(DBG_INFO, "added %s as recip", *envto);
351
return SMFIS_CONTINUE;
355
mlfi_envfrom(SMFICTX * ctx, char **envfrom)
357
struct mlfiPriv *priv;
358
char dirname[BUFFLEN];
359
char messagecopy[BUFFLEN];
360
struct stat buf, StatBuf;
363
return SMFIS_TEMPFAIL;
366
/* open a file to store this message */
367
mystrlcpy(dirname, RUNTIME_DIR, sizeof(dirname));
368
strncat(dirname, TEMPLATE, sizeof(dirname) - 1 - strlen(dirname));
369
if (mymktempdir(dirname) == NULL) {
370
mydebug(DBG_FATAL, "Failed to create temp dir %s: %s", dirname,
372
return SMFIS_TEMPFAIL;
374
snprintf(messagecopy, sizeof(messagecopy), "%s/email.txt", dirname);
375
mydebug(DBG_INFO, "got %s file, %s sender", messagecopy, *envfrom);
377
priv->mlfi_fname = strdup(messagecopy);
378
if (priv->mlfi_fname == NULL) {
379
return SMFIS_TEMPFAIL;
381
priv->mlfi_envfrom = strdup(*envfrom);
382
if (!priv->mlfi_envfrom) {
383
free(priv->mlfi_fname);
384
priv->mlfi_fname = NULL;
385
return SMFIS_TEMPFAIL;
388
if (lstat(dirname, &StatBuf) < 0) {
389
mydebug(DBG_FATAL, "Error while trying lstat(%s): %s", dirname,
391
exit(EX_UNAVAILABLE);
394
/* may be too restrictive for you, but's good to avoid problems */
395
if (!S_ISDIR(StatBuf.st_mode) || StatBuf.st_uid != geteuid() || StatBuf.st_gid != getegid() || !(StatBuf.st_mode & S_IRWXU)) {
397
"Security Warning: %s must be a Directory and owned by "
398
"User %d and Group %d\n"
399
"and just read-/write-able by the User and noone else. "
400
"Exit.", dirname, geteuid(), getegid());
401
exit(EX_UNAVAILABLE);
403
/* there is still a race condition here if RUNTIME_DIR is writeable by the attacker :-\ */
405
if ((priv->mlfi_fp = fopen(priv->mlfi_fname, "w+")) == NULL) {
406
free(priv->mlfi_fname);
407
priv->mlfi_fname = NULL;
408
free(priv->mlfi_envfrom);
409
priv->mlfi_envfrom = NULL;
410
return SMFIS_TEMPFAIL;
415
if (buf.st_uid != geteuid() || (buf.st_mode & 0777) != 0700) {
417
"Security alert -- someone changed %s. Uid is %d, mode is %o",
418
dirname, buf.st_uid, 0777 & buf.st_mode);
419
fclose(priv->mlfi_fp);
420
unlink(priv->mlfi_fname);
422
free(priv->mlfi_fname);
423
free(priv->mlfi_envfrom);
425
return SMFIS_TEMPFAIL;
429
/* save the private data */
431
smfi_setpriv(ctx, priv);
433
/* continue processing */
434
return SMFIS_CONTINUE;
438
mlfi_header(SMFICTX *ctx, char *headerf, char *headerv)
440
/* write the header to the log file */
441
fprintf(MLFIPRIV->mlfi_fp, "%s: %s\n", headerf, headerv);
443
/* continue processing */
444
return SMFIS_CONTINUE;
448
mlfi_eoh(SMFICTX *ctx)
450
/* output the blank line between the header and the body */
451
fprintf(MLFIPRIV->mlfi_fp, "\n");
453
/* continue processing */
454
return SMFIS_CONTINUE;
458
mlfi_body(SMFICTX *ctx, u_char *bodyp, size_t bodylen)
460
/* output body block to log file */
461
u_char *d = bodyp, *s = bodyp;
462
u_char *lastc = bodyp + bodylen - 1;
464
/* convert crlf to lf */
466
if (s != lastc && *s == 13 && *(s+1) == 10)
471
bodylen = (size_t)(d - bodyp);
473
if (bodylen && fwrite(bodyp, bodylen, 1, MLFIPRIV->mlfi_fp) <= 0) {
475
(void) mlfi_cleanup(ctx, 0);
476
return SMFIS_TEMPFAIL;
479
/* continue processing */
480
return SMFIS_CONTINUE;
483
/* Simple "protocol" */
484
const char _EOT = '\3';
487
mlfi_eom(SMFICTX *ctx)
489
struct mlfiPriv *priv = MLFIPRIV;
494
struct sockaddr_un saddr;
499
mydebug(DBG_INFO, "EOM");
500
/* close the file so we can run checks on it!!! */
502
fclose(priv->mlfi_fp);
504
/* AFAIK, AF_UNIX is obsolete. POSIX defines AF_LOCAL */
505
saddr.sun_family = AF_UNIX;
506
mystrlcpy(saddr.sun_path, AMAVISD_SOCKET, sizeof(saddr.sun_path));
507
mydebug(DBG_INFO, "allocate socket()");
508
r = (sock = socket(PF_UNIX, SOCK_STREAM, 0));
510
mydebug(DBG_FATAL, "failed to allocate socket: %s",
514
mydebug(DBG_INFO, "connect()");
515
r = connect(sock, (struct sockaddr *) (&saddr), sizeof(saddr));
517
mydebug(DBG_FATAL, "failed to connect(): %s", strerror(errno));
520
strrchr(priv->mlfi_fname, '/')[0] = 0;
521
/* amavis-perl wants the directory, not the filename */
522
mydebug(DBG_INFO, "senddir()");
523
r = send(sock, priv->mlfi_fname, strlen(priv->mlfi_fname), 0);
525
mydebug(DBG_FATAL, "failed to send() directory: %s",
529
r = recv(sock, &retval, 1, 0);
532
"failed to recv() directory confirmation: %s",
537
sender = (strlen(priv->mlfi_envfrom) > 0) ? priv->mlfi_envfrom : "<>";
538
mydebug(DBG_INFO, "sendfrom() %s", sender);
539
sender_l = strlen(sender);
540
if (sender_l > SOCKBUFLEN) {
541
mydebug(DBG_WARN, "Sender too long (%d), truncated to %d characters", sender_l, SOCKBUFLEN);
542
sender_l = SOCKBUFLEN;
544
r = send(sock, sender, sender_l, 0);
546
mydebug(DBG_FATAL, "failed to send() Sender: %s", strerror(errno));
547
else if (r < sender_l)
548
mydebug(DBG_WARN, "failed to send() complete Sender, truncated to %d characters ", r);
551
r = recv(sock, &retval, 1, 0);
553
mydebug(DBG_FATAL, "failed to recv() ok for Sender info: %s",
557
priv->mlfi_thisenvto = &(priv->mlfi_envto);
558
for (x = 0; (r >= 0) && (x < priv->mlfi_numto); x++) {
560
mydebug(DBG_INFO, "sendto() %s", priv->mlfi_thisenvto->str);
561
recipient_l = strlen(priv->mlfi_thisenvto->str);
562
if (recipient_l > SOCKBUFLEN) {
563
mydebug(DBG_WARN, "Recipient too long (%d), truncated to %d characters", recipient_l,SOCKBUFLEN);
564
recipient_l = SOCKBUFLEN;
566
r = send(sock, priv->mlfi_thisenvto->str, recipient_l, 0);
568
mydebug(DBG_FATAL, "failed to send() Recipient: %s",
572
mydebug(DBG_WARN, "failed to send() complete Recipient, truncated to %d characters ", r);
573
r = recv(sock, &retval, 1, 0);
576
"failed to recv() ok for recip info: %s",
578
priv->mlfi_thisenvto = priv->mlfi_thisenvto->next;
583
mydebug(DBG_INFO, "sendEOT()");
584
r = send(sock, &_EOT, 1, 0);
585
/* send "end of args" msg */
587
mydebug(DBG_FATAL, "failed to send() EOT: %s", strerror(errno));
589
r = recv(sock, buff, 6, 0);
591
mydebug(DBG_FATAL, "Failed to recv() final result: %s",
594
mydebug(DBG_FATAL, "Failed to recv() final result: empty status string");
595
/* get back final result */
599
mydebug(DBG_INFO, "finished conversation\n");
601
return clearpriv(ctx, SMFIS_TEMPFAIL, 0);
602
/* some point of the communication failed miserably - so give up */
605
/* Protect against empty return string */
611
mydebug(DBG_INFO, "retval is %d", retval);
614
* discard it so it doesn't go bouncing around!! */
615
mydebug(DBG_WARN, "discarding mail");
616
return clearpriv(ctx, SMFIS_DISCARD, 0);
618
if (retval == EX_UNAVAILABLE) { /* REJECT handling */
619
/* by Didi Rieder and Mark Martinec */
620
mydebug(DBG_WARN, "rejecting mail");
621
smfi_setreply(ctx, "550", "5.7.1", "Message content rejected");
622
return clearpriv(ctx, SMFIS_REJECT, 0);
625
if (enable_x_header) {
626
if (smfi_chgheader(ctx, X_HEADER_TAG, 1, X_HEADER_LINE) == MI_FAILURE) {
627
mydebug(DBG_INFO, "adding header");
628
smfi_addheader(ctx, X_HEADER_TAG, X_HEADER_LINE);
630
mydebug(DBG_INFO, "header already present");
632
mydebug(DBG_WARN, "returning ACCEPT");
633
return clearpriv(ctx, SMFIS_ACCEPT, 0);
635
/* if we got any exit status but 0, we didn't check the file...
636
* so don't add the header. We return TEMPFAIL instead */
638
mydebug(DBG_WARN, "returning TEMPFAIL");
639
return clearpriv(ctx, SMFIS_TEMPFAIL, 0);
641
mydebug(DBG_WARN, "couldn't scan - no priv object");
642
return clearpriv(ctx, SMFIS_TEMPFAIL, 0); /* no priv object */
646
mlfi_close(SMFICTX *ctx)
648
return clearpriv(ctx, SMFIS_ACCEPT, 1);
652
mlfi_abort(SMFICTX *ctx)
654
return mlfi_cleanup(ctx, 0);
658
mlfi_cleanup(SMFICTX *ctx, bool ok)
660
sfsistat rstat = SMFIS_CONTINUE;
661
struct mlfiPriv *priv = MLFIPRIV;
668
/* close the archive file */
669
if (priv->mlfi_fp != NULL && fclose(priv->mlfi_fp) == EOF) {
670
/* failed; we have to wait until later */
671
rstat = SMFIS_TEMPFAIL;
672
(void) unlink(priv->mlfi_fname);
674
/* add a header to the message announcing our presence */
675
if (gethostname(host, sizeof(host) - 1) < 0)
676
mystrlcpy(host, "localhost", sizeof host);
677
p = strrchr(priv->mlfi_fname, '/');
679
p = priv->mlfi_fname;
683
/* message was aborted -- delete the archive file */
684
(void) unlink(priv->mlfi_fname);
685
*(strrchr(priv->mlfi_fname, '/')) = 0;
686
rmdir(priv->mlfi_fname);
690
mydebug(DBG_INFO, "cleanup called");
691
return clearpriv(ctx, rstat, 0);
695
struct smfiDesc smfilter = {
696
"amavis-milter", /* filter name */
697
SMFI_VERSION, /* version code -- do not change */
698
SMFIF_ADDHDRS|SMFIF_CHGHDRS,/* flags */
699
mlfi_connect, /* connection info filter */
700
NULL, /* SMTP HELO command filter */
701
mlfi_envfrom, /* envelope sender filter */
702
mlfi_envto, /* envelope recipient filter */
703
mlfi_header, /* header filter */
704
mlfi_eoh, /* end of header */
705
mlfi_body, /* body block filter */
706
mlfi_eom, /* end of message */
707
mlfi_abort, /* message aborted */
708
mlfi_close /* connection cleanup */
713
main(int argc, char *argv[])
716
const char *args = "p:d:Dx";
721
#if !defined(HAVE_MKDTEMP) && !defined(HAVE_MKTEMP)
722
int mypid = getpid();
724
srand48(time(NULL) ^ (mypid + (mypid << 15)));
730
/* Process command line options */
731
while ((c = getopt(argc, argv, args)) != -1) {
734
if (optarg == NULL || *optarg == '\0') {
735
mydebug(DBG_FATAL, "Illegal conn: %s", optarg);
738
/* Unlink any existing file that might be in place of
739
* the socket we want to create. This might not exactly
740
* be safe, or friendly, but I'll deal with that later.
741
* Be nice and issue a warning if we have a problem, but
742
* other than that, ignore it. */
743
if ((unlink(optarg)) < 0) {
744
mydebug(DBG_WARN, "unlink(): %s: %s", optarg, strerror(errno));
746
(void) smfi_setconn(optarg);
749
switch (atoi(optarg)) {
751
debuglevel = DBG_INFO;
753
debuglevel = debuglevel | DBG_WARN;
755
debuglevel = debuglevel | DBG_FATAL;
758
debuglevel = DBG_NONE;
761
debuglevel = DBG_ALL;
775
if (smfi_register(smfilter) == MI_FAILURE) {
776
mydebug(DBG_FATAL, "smfi_register failed");
777
exit(EX_UNAVAILABLE);
780
/* See if we're supposed to become a daemonized process */
781
if (AM_DAEMON == 1) {
783
/* 2001/11/09 Anne Bennett: daemonize properly.
784
* OK, let's be a real daemon. Taken from page 417
785
* of Stevens' "Advanced Programming in the UNIX Environment".
788
/* Step 1: Fork and have parent exit. This not only
789
* backgrounds us but makes sure we are not a process group
793
/* Fork ourselves into the background, and see if it worked */
794
if ((pid = fork()) > 0) {
796
mydebug(DBG_INFO, "amavis-milter forked into background");
797
/* We are the parent; exit. */
800
} else if (pid == -1) {
802
mydebug(DBG_FATAL, "fork() returned error: %s", strerror(errno));
803
exit(EX_UNAVAILABLE);
807
/* OK, we're backgrounded.
808
* Step 2: Call setsid to create a new session. This makes
809
* sure among other things that we have no controlling
812
if ( setsid() < (pid_t)0 ) {
813
mydebug(DBG_FATAL, "setsid() returned error: %s", strerror(errno));
814
exit(EX_UNAVAILABLE);
817
/* Step 3: Set the working directory appropriately. */
818
if (chdir("/") < 0 ) {
819
mydebug(DBG_FATAL, "chdir(/) returned error: %s", strerror(errno));
820
exit(EX_UNAVAILABLE);
823
/* Step 4: Close all file descriptors. */
824
for (i = 0; i < _POSIX_OPEN_MAX ; i++) {
828
/* Open /dev/null read-only (fd 0 = STDIN) */
829
if ((devnull = open(DEVNULL, O_RDONLY, 0)) < 0) {
830
mydebug(DBG_FATAL, "Could not open %s as STDIN: %s", DEVNULL,
832
exit(EX_UNAVAILABLE);
835
mydebug(DBG_FATAL, "Got wrong file descriptor as STDIN: %s != 0",
837
exit(EX_UNAVAILABLE);
840
/* Open /dev/null write-only (fd 1 = STDOUT) */
841
if ((devnull = open(DEVNULL, O_WRONLY, 0)) < 0) {
842
mydebug(DBG_FATAL, "Could not open %s as STDOUT: %s", DEVNULL,
844
exit(EX_UNAVAILABLE);
847
mydebug(DBG_FATAL, "Got wrong file descriptor as STDOUT: %s != 1",
849
exit(EX_UNAVAILABLE);
852
/* Open /dev/null write-only (fd 2 = STDERR) */
853
if ((devnull = open(DEVNULL, O_WRONLY, 0)) < 0) {
854
mydebug(DBG_FATAL, "Could not open %s as STDERR: %s", DEVNULL,
856
exit(EX_UNAVAILABLE);
859
mydebug(DBG_FATAL, "Got wrong file descriptor as STDERR: %s != 2",
861
exit(EX_UNAVAILABLE);
866
/* hand control over to libmilter */
867
mydebug(DBG_INFO, "Starting, handing off to smfi_main");