diff options
author | Stef Walter <stef@thewalter.net> | 2025-01-29 06:06:53 +0100 |
---|---|---|
committer | Stef Walter <stef@thewalter.net> | 2025-01-29 06:27:38 +0100 |
commit | 9c21d3312ff12f6d3f56424875ddf13b0c165952 (patch) | |
tree | 42ed381d1c2a86a79fab61f2c8f00d8fb3fdf1a9 /common | |
parent | 65d3956a9befdca748cbbd2993bf0707af021886 (diff) |
Fix up the build
This copies the last version of the 1.10 clamsmtp common/
files into this project. They used to be shared with the proxsmtp
project but that project has since diverged.
Diffstat (limited to 'common')
-rw-r--r-- | common/compat.c | 532 | ||||
-rw-r--r-- | common/compat.h | 143 | ||||
-rw-r--r-- | common/smtppass.c | 2102 | ||||
-rw-r--r-- | common/smtppass.h | 283 | ||||
-rw-r--r-- | common/sock_any.c | 385 | ||||
-rw-r--r-- | common/sock_any.h | 90 | ||||
-rw-r--r-- | common/spio.c | 643 | ||||
-rw-r--r-- | common/sppriv.h | 76 | ||||
-rw-r--r-- | common/stringx.c | 174 | ||||
-rw-r--r-- | common/stringx.h | 53 | ||||
-rw-r--r-- | common/usuals.h | 78 |
11 files changed, 4559 insertions, 0 deletions
diff --git a/common/compat.c b/common/compat.c new file mode 100644 index 0000000..dcd2bb8 --- /dev/null +++ b/common/compat.c @@ -0,0 +1,532 @@ +/* + * Copyright (c) 2004-2005, 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 + * Stef Walter <stef@memberwebs.com> + * + * + * PORTIONS FROM OPENBSD: ------------------------------------------------- + * + * Copyright (c) 1998 Todd C. Miller <Todd.Miller@courtesan.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED ``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 AUTHOR 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. + */ + + + +#include "usuals.h" +#include "compat.h" + +#include <ctype.h> +#include <stdlib.h> + +#ifndef HAVE_REALLOCF + +void* reallocf(void* ptr, size_t size) +{ + void* ret = realloc(ptr, size); + + if(!ret && size) + free(ptr); + + return ret; +} + +#endif + +#ifndef HAVE_STRLWR +char* strlwr(char* s) +{ + char* t = s; + while(*t) + { + *t = tolower(*t); + t++; + } + return s; +} +#endif + +#ifndef HAVE_STRUPR +char* strupr(char* s) +{ + char* t = s; + while(*t) + { + *t = toupper(*t); + t++; + } + return s; +} +#endif + +#ifndef HAVE_STRLCPY + +size_t strlcpy(char* dst, const char* src, size_t siz) +{ + char* d = dst; + const char* s = src; + size_t n = siz; + + /* Copy as many bytes as will fit */ + if(n != 0 && --n != 0) + { + do + { + if((*d++ = *s++) == 0) + break; + } + while(--n != 0); + } + + /* Not enough room in dst, add NUL and traverse rest of src */ + if(n == 0) + { + if(siz != 0) + *d = '\0'; /* NUL-terminate dst */ + while (*s++) + ; + } + + return s - src - 1; /* count does not include NUL */ +} + +#endif + +#ifndef HAVE_STRLCAT + +size_t strlcat(char* dst, const char* src, size_t siz) +{ + char* d = dst; + const char* s = src; + size_t n = siz; + size_t dlen; + + /* Find the end of dst and adjust bytes left but don't go past end */ + while(n-- != 0 && *d != '\0') + d++; + dlen = d - dst; + n = siz - dlen; + + if(n == 0) + return dlen + strlen(s); + while(*s != '\0') + { + if(n != 1) + { + *d++ = *s; + n--; + } + + s++; + } + + *d = '\0'; + + return dlen + (s - src); /* count does not include NUL */ +} + +#endif + +#ifndef HAVE_STRCASESTR + +const char* strcasestr(const char *s, const char *find) +{ + char c, sc; + size_t len; + + if((c = *find++) != 0) + { + c = tolower((unsigned char)c); + len = strlen(find); + do + { + do + { + if((sc = *s++) == 0) + return (NULL); + } + while((char)tolower((unsigned char)sc) != c); + } + while (strncasecmp(s, find, len) != 0); + s--; + } + return((const char*)s); +} + +#endif + +#ifndef HAVE_SETENV + +#include <stdio.h> + +int setenv(const char* name, const char* value, int overwrite) +{ + char* t; + int r; + if(getenv(name) && !overwrite) + return 0; + t = (char*)malloc((strlen(name) + strlen(value) + 2) * sizeof(char)); + if(!t) return -1; + sprintf(t, "%s=%s", name, value); + r = putenv(t); + if(r != 0) + free(t); + return r; +} + +#endif + +#ifndef HAVE_DAEMON + +#include <sys/types.h> +#include <unistd.h> +#include <stdlib.h> +#include <signal.h> +#include <fcntl.h> + +int daemon(int nochdir, int noclose) +{ + struct sigaction osa, sa; + int oerrno, fd, osa_ok; + pid_t newgrp; + + /* A SIGHUP may be thrown when the parent exits below. */ + sigemptyset(&sa.sa_mask); + sa.sa_handler = SIG_IGN; + sa.sa_flags = 0; + osa_ok = sigaction(SIGHUP, &sa, &osa); + + switch(fork()) + { + case -1: + return -1; + case 0: + break; + default: + _exit(0); + } + + newgrp = setsid(); + oerrno = errno; + if(osa_ok != -1) + sigaction(SIGHUP, &osa, NULL); + if(newgrp == -1) + { + errno = oerrno; + return -1; + } + if(!nochdir) + chdir("/"); + if(!noclose && (fd = open(_PATH_DEVNULL, O_RDWR, 0)) != -1) + { + dup2(fd, STDIN_FILENO); + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + if(fd > 2) + close(fd); + } + return 0; + +} + +#endif + +#ifndef HAVE_GETLINE + +ssize_t getline(char** lineptr, size_t* n, FILE* stream) +{ + return getdelim(lineptr, n, '\n', stream); +} + +#endif + +#ifndef HAVE_GETDELIM + +ssize_t getdelim(char** lineptr, size_t* n, int delim, FILE* stream) +{ + size_t written = 0; + int allocated, ch; + ssize_t ret = -1; + char* p; + + if(!n || !lineptr || !stream) + { + errno = EINVAL; + return -1; + } + + /* Track whether we allocated this or not */ + allocated = !(*lineptr); + +#ifdef HAVE_FLOCKFILE + /* Affects performance on Solaris. */ + flockfile(stream); +#endif + + for(;;) + { + if(!*lineptr) + *n = 0; + + /* Reallocate if we need more space */ + if(written == *n - 1) + { + *n = *n ? *n * 2 : 256; + if(!(p = (char*)realloc(*lineptr, *n))) + { + if(allocated && *lineptr) + free(*lineptr); + errno = ENOMEM; + ret = -1; + goto finally; + } + *lineptr = p; + } + + while(written < *n - 1) + { +#ifdef HAVE_FLOCKFILE + ch = getc_unlocked(stream); +#else + ch = fgetc(stream); +#endif + + if(ferror(stream)) + { + if(allocated && *lineptr) + free(*lineptr); + ret = -1; + goto finally; + } + + if(ch != EOF) + (*lineptr)[written++] = ch; + + /* Done, null terminate, return */ + if(ch == EOF || ch == delim) + { + (*lineptr)[written] = 0; + ret = written ? written : -1; + goto finally; + } + } + } +finally: + +#ifdef HAVE_FLOCKFILE + funlockfile(stream); +#endif + return ret; +} + +#endif + +#ifndef HAVE_ERR_H + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <stdarg.h> + +extern char** __argv; + +static const char* calc_prog_name() +{ + static char prognamebuf[256]; + static int prepared = 0; + + if(!prepared) + { + const char* beg = strrchr(__argv[0], '\\'); + const char* temp = strrchr(__argv[0], '/'); + beg = (beg > temp) ? beg : temp; + beg = (beg) ? beg + 1 : __argv[0]; + + temp = strrchr(__argv[0], '.'); + temp = (temp > beg) ? temp : __argv[0] + strlen(__argv[0]); + + if((temp - beg) > 255) + temp = beg + 255; + + strncpy(prognamebuf, beg, temp - beg); + prognamebuf[temp - beg] = 0; + prepared = 1; + } + + return prognamebuf; +} + +static FILE* err_file; /* file to use for error output */ + +/* + * This is declared to take a `void *' so that the caller is not required + * to include <stdio.h> first. However, it is really a `FILE *', and the + * manual page documents it as such. + */ +void err_set_file(void *fp) +{ + if (fp) + err_file = fp; + else + err_file = stderr; +} + +void err(int eval, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + verrc(eval, errno, fmt, ap); + va_end(ap); +} + +void verr(int eval, const char *fmt, va_list ap) +{ + verrc(eval, errno, fmt, ap); +} + +void errc(int eval, int code, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + verrc(eval, code, fmt, ap); + va_end(ap); +} + +void verrc(int eval, int code, const char *fmt, va_list ap) +{ + if (err_file == 0) + err_set_file((FILE *)0); + fprintf(err_file, "%s: ", calc_prog_name()); + if (fmt != NULL) { + vfprintf(err_file, fmt, ap); + fprintf(err_file, ": "); + } + fprintf(err_file, "%s\n", strerror(code)); + exit(eval); +} + +void errx(int eval, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + verrx(eval, fmt, ap); + va_end(ap); +} + +void verrx(int eval, const char *fmt, va_list ap) +{ + if (err_file == 0) + err_set_file((FILE *)0); + fprintf(err_file, "%s: ", calc_prog_name()); + if (fmt != NULL) + vfprintf(err_file, fmt, ap); + fprintf(err_file, "\n"); + exit(eval); +} + +void warn(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vwarnc(errno, fmt, ap); + va_end(ap); +} + +void vwarn(const char *fmt, va_list ap) +{ + vwarnc(errno, fmt, ap); +} + +void warnc(int code, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vwarnc(code, fmt, ap); + va_end(ap); +} + +void vwarnc(int code, const char *fmt, va_list ap) +{ + if (err_file == 0) + err_set_file((FILE *)0); + fprintf(err_file, "%s: ", calc_prog_name()); + if (fmt != NULL) + { + vfprintf(err_file, fmt, ap); + fprintf(err_file, ": "); + } + fprintf(err_file, "%s\n", strerror(code)); +} + +void warnx(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vwarnx(fmt, ap); + va_end(ap); +} + +void vwarnx(const char *fmt, va_list ap) +{ + if(err_file == 0) + err_set_file((FILE*)0); + fprintf(err_file, "%s: ", calc_prog_name()); + if(fmt != NULL) + vfprintf(err_file, fmt, ap); + fprintf(err_file, "\n"); +} + +#endif + diff --git a/common/compat.h b/common/compat.h new file mode 100644 index 0000000..6675c77 --- /dev/null +++ b/common/compat.h @@ -0,0 +1,143 @@ +/* + * 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 + * Stef Walter <stef@memberwebs.com> + * + */ + +#ifndef _COMPAT_H_ +#define _COMPAT_H_ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <sys/types.h> +#include <stdlib.h> + +#ifndef HAVE_STDARG_H +#error ERROR: Must have a working stdarg.h header +#else +#include <stdarg.h> +#endif + +#ifdef WITH_DMALLOC +#include "dmalloc.h" +#endif + +#ifndef HAVE_REALLOCF +void* reallocf(void* p, size_t sz); +#endif + +#include <pthread.h> + +#if HAVE_ERR_MUTEX == 1 +# define MUTEX_TYPE PTHREAD_MUTEX_ERRORCHECK_NP +#else +# if HAVE_ERR_MUTEX == 2 +# define MUTEX_TYPE PTHREAD_MUTEX_ERRORCHECK +# else +# undef MUTEX_TYPE +# endif +#endif + +#ifndef HAVE_STRLWR +char* strlwr(char* s); +#endif + +#ifndef HAVE_STRUPR +char* strupr(char* s); +#endif + +#ifndef HAVE_STRLCAT +size_t strlcat(char *dst, const char *src, size_t size); +#endif + +#ifndef HAVE_STRLCPY +size_t strlcpy(char *dst, const char *src, size_t size); +#endif + +#ifndef HAVE_STRCASESTR +const char* strcasestr(const char *s, const char *find); +#endif + +#ifndef HAVE_SETENV +int setenv(const char* name, const char* value, int overwrite); +#endif + +#ifndef HAVE_DAEMON +int daemon(int nochdir, int noclose); +#endif + +#ifndef HAVE_GETLINE +ssize_t getline(char** lineptr, size_t* n, FILE* stream); +#endif + +#ifndef HAVE_GETDELIM +ssize_t getdelim(char** lineptr, size_t* n, int delim, FILE* stream); +#endif + +#ifdef HAVE_ERR_H +#include <err.h> +#else +#include <stdarg.h> +void err_set_file(void *fp); +void err_set_exit(void (*ef)(int)); +void err(int eval, const char *fmt, ...); +void verr(int eval, const char *fmt, va_list ap); +void errc(int eval, int code, const char *fmt, ...); +void verrc(int eval, int code, const char *fmt, va_list ap); +void errx(int eval, const char *fmt, ...); +void verrx(int eval, const char *fmt, va_list ap); +void warn(const char *fmt, ...); +void vwarn(const char *fmt, va_list ap); +void warnc(int code, const char *fmt, ...); +void vwarnc(int code, const char *fmt, va_list ap); +void warnx(const char *fmt, ...); +void vwarnx(const char *fmt, va_list ap); +#endif + +#ifdef HAVE_PATHS_H +#include <paths.h> +#endif + +#ifndef _PATH_DEVNULL +#define _PATH_DEVNULL "/dev/null" +#endif + +#ifndef _PATH_TMP +#define _PATH_TMP "/tmp" +#endif + +#endif /* _COMPAT_H_ */ diff --git a/common/smtppass.c b/common/smtppass.c new file mode 100644 index 0000000..d9be1ba --- /dev/null +++ b/common/smtppass.c @@ -0,0 +1,2102 @@ +/* + * 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 <stef@memberwebs.com> + * Andreas Steinmetz <ast@domdv.de> + * Rubio Vaughan <rubio@passim.net> + * Olivier Beyssac <ob@r14.freenix.org> + */ + +#define _GNU_SOURCE + +#include <sys/time.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/param.h> +#include <sys/stat.h> + +#include <ctype.h> +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> +#include <fcntl.h> +#include <syslog.h> +#include <signal.h> +#include <errno.h> +#include <stdarg.h> +#include <pwd.h> +#include <time.h> + +#include "usuals.h" + +#ifdef LINUX_TRANSPARENT_PROXY +#include <linux/netfilter_ipv4.h> +#endif + +#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>.<CRLF>" 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 DATA_END_SIG "." CRLF + +#define DATA_RSP "354" +#define OK_RSP "250" +#define START_RSP "220" + +#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 + +/* + * 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" + +/* ----------------------------------------------------------------------- + * 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); + + 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; + + 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; + } + + 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(setgid(pw->pw_gid) == -1 || + setuid(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); + + /* 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; +} + + +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 ? 0 : 1); +} + +static int make_connections(spctx_t* ctx, int client) +{ + struct sockaddr_any peeraddr; + struct sockaddr_any addr; + struct sockaddr_any* outaddr; + char buf[MAXPATHLEN]; + const char* outname; + + 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 */ + outaddr = &(g_state.outaddr); + outname = g_state.outname; + + /* For transparent proxying we have to discover the address to connect to */ + if(g_state.transparent) + { + memset(&addr, 0, sizeof(addr)); + SANY_LEN(addr) = sizeof(addr); + +#ifdef LINUX_TRANSPARENT_PROXY + 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; + } + + /* 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; + } + + outaddr = &addr; + } + + /* Check for loopback option */ + else if(SANY_TYPE(*outaddr) == AF_INET && + outaddr->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)); + outaddr = &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(outaddr != &(g_state.outaddr)) + { + if(sock_any_ntop(outaddr, buf, MAXPATHLEN, 0) != -1) + outname = buf; + else + outname = "unknown"; + } + + /* Connect to the server */ + if(spio_connect(ctx, &(ctx->server), outaddr, outname) == -1) + return -1; + + return 0; +} + +/* ---------------------------------------------------------------------------------- + * SMTP HANDLING + */ + +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 */ + + /* 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))) + { + /* Send back the intermediate response to the client */ + if(spio_write_data(ctx, &(ctx->client), SMTP_DATAINTERMED) == -1) + RETURN(-1); + + /* + * 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; + } + + /* 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); + } + } + + 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 <blah@blah.com> + * 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_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 <CRLF>.<CRLF> 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_fail_data(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; + + /* 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; +} + +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 >= 1024) + errx(2, "invalid setting: " CFG_MAXTHREADS " (must be between 1 and 1024)"); + 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) + { + if((g_state.transparent = strtob(value)) == -1) + errx(2, "invalid value for " CFG_TRANSPARENT); + 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; + } + + /* 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; +} + diff --git a/common/smtppass.h b/common/smtppass.h new file mode 100644 index 0000000..9733447 --- /dev/null +++ b/common/smtppass.h @@ -0,0 +1,283 @@ +/* + * 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 + * Stef Walter <stef@memberwebs.com> + * + */ + +#ifndef __SMTPPASS_H__ +#define __SMTPPASS_H__ + +/* Forward declarations */ +struct sockaddr_any; +struct spctx; + +/* ----------------------------------------------------------------------------- + * BUFFERED MULTIPLEXING IO + * + * This isn't meant to be a replacement library for all sorts of IO + * only things that are currently used go here. + */ + +/* + * A generous maximum line length. It needs to be longer than + * a full path on this system can be, because we pass the file + * name to clamd. + */ + +#if 2000 > MAXPATHLEN + #define SP_LINE_LENGTH 2000 +#else + #define SP_LINE_LENGTH (MAXPATHLEN + 128) +#endif + +typedef struct spio +{ + int fd; /* The file descriptor wrapped */ + const char* name; /* The name for logging */ + time_t last_action; /* Time of last action on descriptor */ + char peername[MAXPATHLEN]; /* Name of the peer on other side of socket */ + char localname[MAXPATHLEN]; /* Address where we accepted the connection */ + + /* Internal use only */ + char line[SP_LINE_LENGTH]; + char* _nx; + size_t _ln; +} +spio_t; + +#define spio_valid(io) ((io) && (io)->fd != -1) + +/* Setup the io structure (allocated elsewhere) */ +void spio_init(spio_t* io, const char* name); + +/* Attach an open descriptor to a socket, optionally returning the peer */ +void spio_attach(struct spctx* ctx, spio_t* io, int fd, struct sockaddr_any* peer); + +/* Connect and disconnect from sockets */ +int spio_connect(struct spctx* ctx, spio_t* io, const struct sockaddr_any* sany, const char* addrname); +void spio_disconnect(struct spctx* ctx, spio_t* io); + +#define SPIO_TRIM 0x00000001 +#define SPIO_DISCARD 0x00000002 +#define SPIO_QUIET 0x00000004 + +/* Read a line from a socket. Use options above. Line + * will be found in io->line */ +int spio_read_line(struct spctx* ctx, spio_t* io, int opts); + +/* Write data to socket (must supply line endings if needed). + * Guaranteed to accept all data or fail. */ +int spio_write_data(struct spctx* ctx, spio_t* io, const char* data); +int spio_write_dataf(struct spctx* ctx, spio_t* io, const char* fmt, ...); +int spio_write_data_raw(struct spctx* ctx, spio_t* io, const unsigned char* buf, int len); + +/* Empty the given socket */ +void spio_read_junk(struct spctx* sp, spio_t* io); + +/* Pass up to 31 spio_t*, followed by NULL. Returns bitmap of ready for reading */ +unsigned int spio_select(struct spctx* ctx, ...); + + +/* ----------------------------------------------------------------------------- + * SMTP PASS THROUGH FUNCTIONALITY + */ + +/* Log lines have to be under roughly 900 chars otherwise + * they get truncated by syslog. */ +#define SP_LOG_LINE_LEN 768 + +typedef struct spctx +{ + unsigned int id; /* Identifier for the connection */ + + spio_t client; /* Connection to client */ + spio_t server; /* Connection to server */ + + FILE* cachefile; /* The file handle for the cached file */ + char cachename[MAXPATHLEN]; /* The name of the file that we cache into */ + char logline[SP_LOG_LINE_LEN]; /* Log line */ + + char* sender; /* The email of the sender */ + char* recipients; /* The email of the recipients */ + char* xforwardaddr; /* The IP address proxied for */ + char* xforwardhelo; /* The HELO/EHLO proxied for */ + + int _crlf; /* Private data */ +} +spctx_t; + +/* + * sp_init initializes the SMTP Pass-Through functionality + * The name passed is the name of the app + */ +void sp_init(const char* name); + +/* + * This starts up the smtp pass thru program. It will call + * the cb_* functions as appropriate. + */ +int sp_run(const char* configfile, const char* pidfile, int dbg_level); + +/* + * Mark the application as shutting down. + * A signal will interupt most IO. + */ +void sp_quit(); + +/* + * Check if the application has been marked to quit. + * Useful for checking after interupted IO. + */ +int sp_is_quit(); + +/* + * Called to cleanup SMTP Pass-Through functionality just + * before the application quits. + */ +void sp_done(); + +/* + * clamsmtpd used to accept command line args. In order to + * process those args it needs to send the values to the + * config file routines. This is how it does that. + */ +#ifdef SP_LEGACY_OPTIONS +int sp_parse_option(const char* name, const char* option); +#endif + + +/* + * The following functions are to be called from within + * the spc_check_data function. + */ + +/* + * Adds a piece of info to the log line + */ +void sp_add_log(spctx_t* ctx, char* prefix, char* line); + +/* + * Reads a line of DATA from client. Or less than a line if + * line is longer than LINE_LENGTH. No trimming or anything + * is done on the read line. This will end automatically + * when <CRLF>.<CRLF> is detected (in which case 0 will + * be returned). The data is returned in data. + */ +int sp_read_data(spctx_t* ctx, const char** data); + +/* + * Writes a line (or piece) of data to a file buffer which is + * later sent to the client using sp_done_data. Calling it with + * a NULL buffer closes the cache file. Guaranteed to accept + * all data given to it or fail. + */ +int sp_write_data(spctx_t* ctx, const char* buf, int buflen); + +/* + * Sends all DATA from the client into the cache. The cache + * file is then available (in spctx_t->cachename) for use. + */ +int sp_cache_data(spctx_t* ctx); + +/* + * Sends the data in file buffer off to server. This is + * completes a successful mail transfer. + */ +int sp_done_data(spctx_t* ctx, const char *header); + +/* + * Fails the data, deletes any temp data, and sends given + * status to client or if NULL then SMTP_DATAFAILED + */ +int sp_fail_data(spctx_t* ctx, const char* smtp_status); + +/* + * Setup the environment with context info. This is useful + * if you're going to fork another process. Be sure to exec + * soon after to prevent the strings from going out of scope. + */ +void sp_setup_forked(spctx_t* ctx, int file); + +/* + * Log a message. levels are syslog levels. Syntax is just + * like printf etc.. Can specify a ctx of NULL in which case + * no connection prefix is prepended. + */ +void sp_message(spctx_t* ctx, int level, const char* msg, ...); +void sp_messagex(spctx_t* ctx, int level, const char* msg, ...); + +/* + * Lock or unlock the main mutex around thread common + * functionality. + */ +void sp_lock(); +void sp_unlock(); + + +/* ----------------------------------------------------------------------------- + * CALLBACKS IMPLMEMENTED BY PROGRAM + */ + +/* + * The following functions create and destroy contexts for a new + * thread. Perform initialization in there and return a spctx + * structure for the thread to use. Return NULL failed. Be sure + * to log message. + */ +extern spctx_t* cb_new_context(); +extern void cb_del_context(spctx_t* ctx); + +/* + * Called when the data section of an email is being transferred. + * Once inside this function you can transfer files using + * sp_read_data, sp_write_data. + * + * After scanning or figuring out the status call either + * sp_done_data or sp_fail_data. Most failures should be handled + * internally using sp_fail_data, unless it's an out of memory + * condition, or sp_fail_data failed. + */ +extern int cb_check_data(spctx_t* ctx); + +/* + * Parse options from the config file. The memory for these + * options will stay around until sp_done is called. Return 0 + * for unrecognized options, 1 for recognized, and quit the + * program for invalid. + */ +extern int cb_parse_option(const char* name, const char* value); + + +#endif /* __SMTPPASS_H__ */ diff --git a/common/sock_any.c b/common/sock_any.c new file mode 100644 index 0000000..82501c1 --- /dev/null +++ b/common/sock_any.c @@ -0,0 +1,385 @@ +/* + * 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 + * Stef Walter <stef@memberwebs.com> + * + */ + +#include <sys/types.h> +#include <sys/socket.h> + +#include <ctype.h> +#include <stdlib.h> +#include <errno.h> +#include <netdb.h> +#include <string.h> +#include <stdio.h> + +#include "sock_any.h" + +#include <arpa/inet.h> + +#define LOCALHOST_ADDR 0x7F000001 + +int sock_any_pton(const char* addr, struct sockaddr_any* any, int opts) +{ + size_t l; + char buf[256]; + char* t; + char* t2; + int defport = (opts & 0xFFFF); + + memset(any, 0, sizeof(*any)); + + /* Just a port? */ + do + { + #define PORT_CHARS "0123456789" + #define PORT_MIN 1 + #define PORT_MAX 5 + + int port = 0; + + l = strspn(addr, PORT_CHARS); + if(l < PORT_MIN || l > PORT_MAX || addr[l] != 0) + break; + + port = strtol(addr, &t2, 10); + if(*t2 || port <= 0 || port >= 65536) + break; + + any->s.in.sin_port = htons(port); + + /* Fill in the type based on defaults */ +#ifdef HAVE_INET6 + if(opts & SANY_OPT_DEFINET6) + any->s.in.sin_family = AF_INET6; + else +#endif + any->s.in.sin_family = AF_INET; + + /* Fill in the address based on defaults */ + if(opts & SANY_OPT_DEFLOCAL) + { +#ifdef HAVE_INET6 + if(opts & SANY_OPT_DEFINET6) + memcpy(&(any->s.in.sin6_addr), &in6addr_loopback, sizeof(struct in6_addr)); + else +#endif + any->s.in.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + } + + /* + * Note the 'any' option is the default since we zero out + * the entire structure above. + */ + + any->namelen = sizeof(any->s.in); + return AF_INET; + } + while(0); + + /* Look and see if we can parse an ipv4 address */ + do + { + #define IPV4_PORT_CHARS + #define IPV4_CHARS "0123456789." + #define IPV4_MIN 3 + #define IPV4_MAX 21 + + int port = 0; + t = NULL; + + l = strlen(addr); + if(l < IPV4_MIN || l > IPV4_MAX) + break; + + strcpy(buf, addr); + + /* Find the last set that contains just numbers */ + l = strspn(buf, IPV4_CHARS); + if(l < IPV4_MIN) + break; + + /* Either end of string or port */ + if(buf[l] != 0 && buf[l] != ':') + break; + + /* Get the port out */ + if(buf[l] != 0) + { + t = buf + l + 1; + buf[l] = 0; + } + + if(t) + { + port = strtol(t, &t2, 10); + if(*t2 || port <= 0 || port >= 65536) + break; + } + + any->s.in.sin_family = AF_INET; + any->s.in.sin_port = htons((unsigned short)(port <= 0 ? defport : port)); + + if(inet_pton(AF_INET, buf, &(any->s.in.sin_addr)) <= 0) + break; + + any->namelen = sizeof(any->s.in); + return AF_INET; + } + while(0); + +#ifdef HAVE_INET6 + do + { + #define IPV6_CHARS "0123456789:" + #define IPV6_MIN 3 + #define IPV6_MAX 51 + + int port = -1; + t = NULL; + + l = strlen(addr); + if(l < IPV6_MIN || l > IPV6_MAX) + break; + + /* If it starts with a '[' then we can get port */ + if(buf[0] == '[') + { + port = 0; + addr++; + } + + strcpy(buf, addr); + + /* Find the last set that contains just numbers */ + l = strspn(buf, IPV6_CHARS); + if(l < IPV6_MIN) + break; + + /* Either end of string or port */ + if(buf[l] != 0) + { + /* If had bracket, then needs to end with a bracket */ + if(port != 0 || buf[l] != ']') + break; + + /* Get the port out */ + t = buf + l + 1; + + if(*t = ':') + t++; + } + + if(t) + { + port = strtol(t, &t, 10); + if(*t || port <= 0 || port >= 65536) + break; + } + + any->s.in6.sin6_family = AF_INET6; + any->s.in6.sin6_port = htons((unsigned short)port <= 0 : defport : port); + + if(inet_pton(AF_INET6, buf, &(any->s.in6.sin6_addr)) >= 0) + break; + + any->namelen = sizeof(any->s.in6); + return AF_INET6; + } + while(0); +#endif + + /* A unix socket path */ + do + { + /* No colon and must have a path component */ + if(strchr(addr, ':') || !strchr(addr, '/')) + break; + + l = strlen(addr); + if(l >= sizeof(any->s.un.sun_path)) + break; + + any->s.un.sun_family = AF_UNIX; + strcpy(any->s.un.sun_path, addr); + + any->namelen = sizeof(any->s.un) - (sizeof(any->s.un.sun_path) - l); + return AF_UNIX; + } + while(0); + + /* A DNS name and a port? */ + do + { + struct addrinfo* res; + int port = 0; + t = NULL; + + l = strlen(addr); + if(l >= 255 || !isalpha(addr[0])) + break; + + /* Some basic illegal character checks */ + if(strcspn(addr, " /\\") != l) + break; + + strcpy(buf, addr); + + /* Find the last set that contains just numbers */ + t = strchr(buf, ':'); + if(t) + { + *t = 0; + t++; + } + + if(t) + { + port = strtol(t, &t2, 10); + if(*t2 || port <= 0 || port >= 65536) + break; + } + + /* Try and resolve the domain name */ + if(getaddrinfo(buf, NULL, NULL, &res) != 0 || !res) + break; + + memcpy(&(any->s.a), res->ai_addr, sizeof(struct sockaddr)); + any->namelen = res->ai_addrlen; + freeaddrinfo(res); + + port = htons((unsigned short)(port <= 0 ? defport : port)); + + switch(any->s.a.sa_family) + { + case PF_INET: + any->s.in.sin_port = port; + break; +#ifdef HAVE_INET6 + case PF_INET6: + any->s.in6.sin6_port = port; + break; +#endif + }; + + return any->s.a.sa_family; + } + while(0); + + return -1; +} + +int sock_any_ntop(const struct sockaddr_any* any, char* addr, size_t addrlen, int opts) +{ + int len = 0; + int port = 0; + + switch(any->s.a.sa_family) + { + case AF_UNIX: + len = strlen(any->s.un.sun_path); + if(addrlen < len + 1) + { + errno = ENOSPC; + return -1; + } + + strcpy(addr, any->s.un.sun_path); + break; + + case AF_INET: + if(inet_ntop(any->s.a.sa_family, &(any->s.in.sin_addr), addr, addrlen) == NULL) + return -1; + port = ntohs(any->s.in.sin_port); + break; + +#ifdef HAVE_INET6 + case AF_INET6: + if(inet_ntop(any->s.a.sa_family, &(any->s.in6.sin6_addr), addr, addrlen) == NULL) + return -1; + port = ntohs(any->s.in6.sin6_port); + break; +#endif + + default: + errno = EAFNOSUPPORT; + return -1; + } + + if(!(opts & SANY_OPT_NOPORT) && port != 0) + { + strncat(addr, ":", addrlen); + addr[addrlen - 1] = 0; + + len = strlen(addr); + addr += len; + addrlen -= len; + + snprintf(addr, addrlen, "%d", port); + } + + return 0; +} + +int sock_any_cmp(const struct sockaddr_any* a1, const struct sockaddr_any* a2, int opts) +{ + if(a1->s.a.sa_family != a2->s.a.sa_family) + return -1; + + switch(a1->s.a.sa_family) + { + case AF_UNIX: + return strcmp(a1->s.un.sun_path, a2->s.un.sun_path); + + case AF_INET: + if(memcmp(&(a1->s.in.sin_addr), &(a2->s.in.sin_addr), sizeof(a2->s.in.sin_addr)) != 0) + return -1; + if(!(opts && SANY_OPT_NOPORT) && a1->s.in.sin_port != a2->s.in.sin_port) + return -1; + return 0; +#ifdef HAVE_INET6 + case AF_INET6: + if(memcmp(&(a1->s.in6.sin6_addr), &(a2->s.in6.sin6_addr), sizeof(a2->s.in6.sin6_addr)) != 0) + return -1; + if(!(opts && SANY_OPT_NOPORT) && a1->s.in6.sin6_port != a2->s.in6.sin6_port) + return -1; + return 0; +#endif + default: + errno = EAFNOSUPPORT; + return -1; + } +} diff --git a/common/sock_any.h b/common/sock_any.h new file mode 100644 index 0000000..77c3841 --- /dev/null +++ b/common/sock_any.h @@ -0,0 +1,90 @@ +/* + * 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 + * Stef Walter <stef@memberwebs.com> + * + */ + +#ifndef __SOCK_ANY_H__ +#define __SOCK_ANY_H__ + +#include <sys/socket.h> +#include <sys/un.h> +#include <netinet/in.h> + +struct sockaddr_any +{ + union _sockaddr_any + { + /* The header */ + struct sockaddr a; + + /* The different types */ + struct sockaddr_un un; + struct sockaddr_in in; +#ifdef HAVE_INET6 + struct sockaddr_in6 in6; +#endif + } s; + size_t namelen; +}; + +#define SANY_ADDR(any) ((any).s.a) +#define SANY_LEN(any) ((any).namelen) +#define SANY_TYPE(any) ((any).s.a.sa_family) + +int sock_any_pton(const char* addr, struct sockaddr_any* any, int opts); + +/* The default port to fill in when no IP/IPv6 port specified */ +#define SANY_OPT_DEFPORT(p) (int)((p) & 0xFFFF) + +/* When only port specified default to IPANY */ +#define SANY_OPT_DEFANY 0x00000000 + +/* When only port specified default to LOCALHOST */ +#define SANY_OPT_DEFLOCAL 0x00100000 + +/* When only port specified default to IPv6 */ +#ifdef HAVE_INET6 +#define SANY_OPT_DEFINET6 0x00200000 +#endif + +int sock_any_ntop(const struct sockaddr_any* any, char* addr, size_t addrlen, int opts); + +/* Don't print or compare the port */ +#define SANY_OPT_NOPORT 0x01000000 + +int sock_any_cmp(const struct sockaddr_any* a1, const struct sockaddr_any* a2, int opts); + +#endif /* __SOCK_ANY_H__ */ diff --git a/common/spio.c b/common/spio.c new file mode 100644 index 0000000..e25ef3d --- /dev/null +++ b/common/spio.c @@ -0,0 +1,643 @@ +/* + * 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 + * Stef Walter <stef@memberwebs.com> + */ + +/* + * select() and stdio are basically mutually exclusive. + * Hence all of this code to try to get some buffering + * along with select IO multiplexing. + */ + +#include <sys/time.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/param.h> +#include <sys/stat.h> + +#include <ctype.h> +#include <stdio.h> +#include <unistd.h> +#include <fcntl.h> +#include <syslog.h> +#include <errno.h> +#include <stdarg.h> +#include <unistd.h> +#include <errno.h> + +#include "compat.h" +#include "usuals.h" +#include "sock_any.h" +#include "stringx.h" +#include "sppriv.h" + +#define MAX_LOG_LINE 79 +#define GET_IO_NAME(io) ((io)->name ? (io)->name : "??? ") +#define HAS_EXTRA(io) ((io)->_ln > 0) + +static void close_raw(int* fd) +{ + ASSERT(fd); + shutdown(*fd, SHUT_RDWR); + close(*fd); + *fd = -1; +} + +static void log_io_data(spctx_t* ctx, spio_t* io, const char* data, int read) +{ + char buf[MAX_LOG_LINE + 1]; + int pos, len; + + ASSERT(ctx && io && data); + + for(;;) + { + data += strspn(data, "\r\n"); + + if(!*data) + break; + + pos = strcspn(data, "\r\n"); + + len = pos < MAX_LOG_LINE ? pos : MAX_LOG_LINE; + memcpy(buf, data, len); + buf[len] = 0; + + sp_messagex(ctx, LOG_DEBUG, "%s%s%s", GET_IO_NAME(io), + read ? " < " : " > ", buf); + + data += pos; + } +} + +void spio_init(spio_t* io, const char* name) +{ + ASSERT(io && name); + memset(io, 0, sizeof(*io)); + io->name = name; + io->fd = -1; +} + +void spio_attach(spctx_t* ctx, spio_t* io, int fd, struct sockaddr_any* peer) +{ + struct sockaddr_any peeraddr; + struct sockaddr_any locaddr; + + io->fd = fd; + + /* Get the address on which we accepted the connection */ + memset(&locaddr, 0, sizeof(locaddr)); + SANY_LEN(locaddr) = sizeof(locaddr); + + if(getsockname(fd, &SANY_ADDR(locaddr), &SANY_LEN(locaddr)) == -1 || + sock_any_ntop(&locaddr, io->localname, MAXPATHLEN, SANY_OPT_NOPORT) == -1) + { + if (errno != EAFNOSUPPORT) + sp_message(ctx, LOG_WARNING, "%s: couldn't get socket address", GET_IO_NAME(io)); + strlcpy(io->localname, "UNKNOWN", MAXPATHLEN); + } + + /* If the caller doesn't want the peer then use our own */ + if (peer == NULL) + peer = &peeraddr; + + memset(peer, 0, sizeof(*peer)); + SANY_LEN(*peer) = sizeof(*peer); + + if(getpeername(fd, &SANY_ADDR(*peer), &SANY_LEN(*peer)) == -1 || + sock_any_ntop(peer, io->peername, MAXPATHLEN, SANY_OPT_NOPORT) == -1) + { + if (errno != EAFNOSUPPORT) + sp_message(ctx, LOG_WARNING, "%s: couldn't get peer address", GET_IO_NAME(io)); + strlcpy(io->peername, "UNKNOWN", MAXPATHLEN); + } + + /* As a double check */ + io->line[0] = 0; + io->_nx = NULL; + io->_ln = 0; +} + +int spio_connect(spctx_t* ctx, spio_t* io, const struct sockaddr_any* sany, + const char* addrname) +{ + int ret = 0; + int fd; + + ASSERT(ctx && io && sany && addrname); + ASSERT(io->fd == -1); + + if((fd = socket(SANY_TYPE(*sany), SOCK_STREAM, 0)) == -1) + RETURN(-1); + + if(setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &(g_state.timeout), sizeof(g_state.timeout)) == -1 || + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &(g_state.timeout), sizeof(g_state.timeout)) == -1) + sp_messagex(ctx, LOG_DEBUG, "%s: couldn't set timeouts on connection", GET_IO_NAME(io)); + + fcntl(fd, F_SETFD, fcntl(fd, F_GETFD, 0) | FD_CLOEXEC); + + if(connect(fd, &SANY_ADDR(*sany), SANY_LEN(*sany)) == -1) + { + close_raw(&fd); + RETURN(-1); + } + + spio_attach(ctx, io, fd, NULL); + +cleanup: + if(ret < 0) + { + if(spio_valid(io)) + close_raw(&(io->fd)); + + sp_message(ctx, LOG_ERR, "%s: couldn't connect to: %s", GET_IO_NAME(io), addrname); + return -1; + } + + ASSERT(io->fd != -1); + sp_messagex(ctx, LOG_DEBUG, "%s connected to: %s", GET_IO_NAME(io), io->peername); + return 0; +} + +void spio_disconnect(spctx_t* ctx, spio_t* io) +{ + ASSERT(ctx && io); + + if(spio_valid(io)) + { + close_raw(&(io->fd)); + sp_messagex(ctx, LOG_DEBUG, "%s connection closed", GET_IO_NAME(io)); + } +} + +unsigned int spio_select(spctx_t* ctx, ...) +{ + fd_set mask; + spio_t* io; + int ret = 0; + int have = 0; + int i = 0; + va_list ap; + struct timeval timeout; + + ASSERT(ctx); + FD_ZERO(&mask); + + va_start(ap, ctx); + + while((io = va_arg(ap, spio_t*)) != NULL) + { + if(spio_valid(io)) + { + /* We can't handle more than 31 args */ + if(i > (sizeof(int) * 8) - 2) + break; + + /* Check if the buffer has something in it */ + if(HAS_EXTRA(io)) + ret |= (1 << i); + + /* Mark for select */ + FD_SET(io->fd, &mask); + have = 1; + } + + i++; + } + + va_end(ap); + + /* If any buffers had something present, then return */ + if(ret != 0) + return ret; + + /* No valid file descriptors */ + if(!have) + return ~0; + + for(;;) + { + /* Select can modify the timeout argument so we copy */ + memcpy(&timeout, &(g_state.timeout), sizeof(timeout)); + + /* Otherwise wait on more data */ + switch(select(FD_SETSIZE, &mask, NULL, NULL, &timeout)) + { + case 0: + sp_messagex(ctx, LOG_ERR, "network operation timed out"); + return ~0; + + case -1: + if(errno == EINTR) + { + if(!sp_is_quit()) + continue; + } + + else + sp_message(ctx, LOG_ERR, "couldn't select on sockets"); + + return ~0; + }; + + break; + } + + /* See what came in */ + i = 0; + + va_start(ap, ctx); + + while((io = va_arg(ap, spio_t*)) != NULL) + { + if(spio_valid(io)) + { + /* We can't handle more than 31 args */ + if(i > (sizeof(int) * 8) - 2) + break; + + /* We have data on the descriptor, which is an action */ + io->last_action = time(NULL); + + /* Check if the buffer has something in it */ + if(FD_ISSET(io->fd, &mask)) + ret |= (1 << i); + } + + i++; + } + + va_end(ap); + + return ret; +} + +int read_raw(spctx_t* ctx, spio_t* io, int opts) +{ + int len, x, count; + char* at; + char* p; + + /* + * Just a refresher: + * + * _nx: Extra data read on last read. + * _ln: Length of that extra data. + * + * _nx should never be equal to line when entering this function. + * And _ln should always be less than a full buffer. + */ + + count = 0; + io->line[0] = 0; + + /* Remaining data in the buffer */ + if(io->_nx && io->_ln > 0) + { + ASSERT(!io->_nx || io->_nx > io->line); + ASSERT(io->_ln < SP_LINE_LENGTH); + ASSERT(io->_nx + io->_ln <= io->line + SP_LINE_LENGTH); + + /* Check for a return in the current buffer */ + if((p = (char*)memchr(io->_nx, '\n', io->_ln)) != NULL) + { + /* Move data to front */ + x = (p - io->_nx) + 1; + ASSERT(x > 0); + memmove(io->line, io->_nx, x); + + /* Null teriminate it */ + io->line[x] = 0; + + /* Do maintanence for next time around */ + io->_ln -= x; + io->_nx += x; + + /* A double check on the return value */ + count += x; + return count; + } + + /* Otherwise move all old data to front */ + memmove(io->line, io->_nx, io->_ln); + count += io->_ln; + + /* We always leave space for a null terminator */ + len = (SP_LINE_LENGTH - io->_ln) - 1; + at = io->line + io->_ln; + } + + /* No data at front just read straight in */ + else + { + /* We always leave space for a null terminator */ + len = SP_LINE_LENGTH - 1; + at = io->line; + } + + for(;;) + { + /* Read a block of data */ + ASSERT(io->fd != -1); + x = read(io->fd, at, sizeof(char) * len); + + if(x == -1) + { + if(errno == EINTR) + { + /* When the application is quiting */ + if(sp_is_quit()) + return -1; + + /* For any other signal we go again */ + continue; + } + + if(errno == ECONNRESET) /* Not usually a big deal so supresse the error */ + sp_messagex(ctx, LOG_DEBUG, "%s: connection disconnected by peer", GET_IO_NAME(io)); + else if(errno == EAGAIN) + sp_messagex(ctx, LOG_WARNING, "%s: network read operation timed out", GET_IO_NAME(io)); + else + sp_message(ctx, LOG_ERR, "%s: couldn't read data from socket", GET_IO_NAME(io)); + + /* + * The basic logic here is that if we've had a fatal error + * reading from the socket once then we shut it down as it's + * no good trying to read from again later. + */ + close_raw(&(io->fd)); + + return -1; + } + + /* End of data */ + else if(x == 0) + { + /* Maintenance for remaining data */ + io->_nx = NULL; + io->_ln = 0; + + return count; + } + + /* Read data which is a descriptor action */ + io->last_action = time(NULL); + + /* Check for a new line */ + p = (char*)memchr(at, '\n', x); + if(p != NULL) + { + p++; + count += (p - at); + + /* Insert the null terminator */ + len = x - (p - at); + memmove(p + 1, p, len); + *p = 0; + + /* Do maintenence for remaining data */ + io->_nx = p + 1; + io->_ln = len; + + return count; + } + + /* Move the buffer pointer along */ + at += x; + len -= x; + count += x; + + if(len <= 0) + { + /* Keep reading until we hit a new line */ + if(opts & SPIO_DISCARD) + { + /* + * K, basically the logic is that we're discarding + * data and the data will be screwed up. So overwriting + * some valid data in order to flush the line and + * keep the buffering simple is a price we pay gladly :) + */ + + ASSERT(128 < SP_LINE_LENGTH); + at = (io->line + SP_LINE_LENGTH) - 128; + len = 128; + + /* Go for next read */ + continue; + } + + io->_nx = NULL; + io->_ln = 0; + + /* Null terminate */ + io->line[SP_LINE_LENGTH] = 0; + + /* A double check on the return value */ + return count; + } + } +} + +int spio_read_line(spctx_t* ctx, spio_t* io, int opts) +{ + int x, l; + char* t; + + ASSERT(ctx && io); + + if(!spio_valid(io)) + { + sp_messagex(ctx, LOG_WARNING, "%s: tried to read from a closed connection", GET_IO_NAME(io)); + return 0; + } + + x = read_raw(ctx, io, opts); + + if(x > 0) + { + if(opts & SPIO_TRIM) + { + t = io->line; + + while(*t && isspace(*t)) + t++; + + /* Bump the entire line down */ + l = t - io->line; + memmove(io->line, t, (x + 1) - l); + x -= l; + + /* Now the end */ + t = io->line + x; + + while(t > io->line && isspace(*(t - 1))) + { + *(--t) = 0; + x--; + } + } + + if(!(opts & SPIO_QUIET)) + log_io_data(ctx, io, io->line, 1); + } + + return x; +} + +int spio_write_data(spctx_t* ctx, spio_t* io, const char* data) +{ + int len = strlen(data); + ASSERT(ctx && io && data); + + if(!spio_valid(io)) + { + sp_message(ctx, LOG_ERR, "%s: connection closed. can't write data", GET_IO_NAME(io)); + return -1; + } + + log_io_data(ctx, io, data, 0); + return spio_write_data_raw(ctx, io, (unsigned char*)data, len); +} + +int spio_write_dataf(struct spctx* ctx, spio_t* io, const char* fmt, ...) +{ + char buf[SP_LINE_LENGTH]; + va_list ap; + ASSERT(ctx && io && fmt); + + buf[0] = 0; + + va_start(ap, fmt); + vsnprintf(buf, SP_LINE_LENGTH, fmt, ap); + va_end(ap); + + buf[SP_LINE_LENGTH - 1] = 0; + + return spio_write_data(ctx, io, buf); +} + +int spio_write_data_raw(spctx_t* ctx, spio_t* io, const unsigned char* buf, int len) +{ + int r; + + ASSERT(ctx && io && buf); + + if(io->fd == -1) + return 0; + + io->last_action = time(NULL); + + while(len > 0) + { + r = write(io->fd, buf, len); + + if(r > 0) + { + buf += r; + len -= r; + } + + else if(r == -1) + { + if(errno == EINTR) + { + /* When the application is quiting */ + if(sp_is_quit()) + return -1; + + /* For any other signal we go again */ + continue; + } + + /* + * The basic logic here is that if we've had a fatal error + * writing to the socket once then we shut it down as it's + * no good trying to write to it again later. + */ + close_raw(&(io->fd)); + + if(errno == EAGAIN) + sp_messagex(ctx, LOG_WARNING, "%s: network write operation timed out", GET_IO_NAME(io)); + else + sp_message(ctx, LOG_ERR, "%s: couldn't write data to socket", GET_IO_NAME(io)); + + return -1; + } + } + + return 0; +} + +void spio_read_junk(spctx_t* ctx, spio_t* io) +{ + char buf[16]; + const char* t; + int said = 0; + int l; + + ASSERT(ctx); + ASSERT(io); + + /* Truncate any data in buffer */ + io->_ln = 0; + io->_nx = 0; + + if(!spio_valid(io)) + return; + + /* Make it non blocking */ + fcntl(io->fd, F_SETFL, fcntl(io->fd, F_GETFL, 0) | O_NONBLOCK); + + for(;;) + { + l = read(io->fd, buf, sizeof(buf) - 1); + if(l <= 0) + break; + + io->last_action = time(NULL); + + buf[l] = 0; + t = trim_start(buf); + + if(!said && *t) + { + sp_messagex(ctx, LOG_DEBUG, "%s: received junk data from daemon", GET_IO_NAME(io)); + said = 1; + } + } + + fcntl(io->fd, F_SETFL, fcntl(io->fd, F_GETFL, 0) & ~O_NONBLOCK); +} diff --git a/common/sppriv.h b/common/sppriv.h new file mode 100644 index 0000000..28351cf --- /dev/null +++ b/common/sppriv.h @@ -0,0 +1,76 @@ +/* + * 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 + * Stef Walter <stef@memberwebs.com> + * + */ + +#ifndef __SPPRIV_H__ +#define __SPPRIV_H__ + +#include "smtppass.h" + +typedef struct spstate +{ + /* Settings ------------------------------- */ + int debug_level; /* The level to print stuff to console */ + int max_threads; /* Maximum number of threads to process at once */ + struct timeval timeout; /* Timeout for communication */ + int keepalives; /* Send server keep alives at this interval */ + int transparent; /* Transparent proxying */ + int xclient; /* Send XFORWARD info */ + const char* directory; /* The temp directory */ + const char* user; /* User to run as */ + const char* pidfile; /* The pid file for daemon */ + const char* header; /* A header to include in the email */ + + struct sockaddr_any outaddr; /* The outgoing address */ + const char* outname; + struct sockaddr_any listenaddr; /* Address to listen on */ + const char* listenname; + + /* State --------------------------------- */ + const char* name; /* The name of the program */ + int quit; /* Quit the process */ + int daemonized; /* Whether process is daemonized or not */ + + /* Internal Use ------------------------- */ + char* _p; +} +spstate_t; + +extern spstate_t g_state; + +#endif /* __SPPRIV_H__ */ + diff --git a/common/stringx.c b/common/stringx.c new file mode 100644 index 0000000..27bf06b --- /dev/null +++ b/common/stringx.c @@ -0,0 +1,174 @@ +/* + * 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 + * Stef Walter <stef@memberwebs.com> + * + */ + +#include <sys/types.h> + +#include <ctype.h> +#include <syslog.h> +#include <stdlib.h> +#include <stdio.h> +#include <strings.h> + +#include "usuals.h" +#include "compat.h" +#include "stringx.h" + +/* ---------------------------------------------------------------------------------- + * Parsing + */ + +int is_first_word(const char* line, const char* word, int len) +{ + ASSERT(line); + ASSERT(word); + ASSERT(len > 0); + + while(*line && isspace(*line)) + line++; + + if(strncasecmp(line, word, len) != 0) + return 0; + + line += len; + return !*line || isspace(*line); +} + +int check_first_word(const char* line, const char* word, int len, char* delims) +{ + const char* t; + int found = 0; + + ASSERT(line); + ASSERT(word); + ASSERT(len > 0); + + t = line; + + while(*t && strchr(delims, *t)) + t++; + + if(strncasecmp(t, word, len) != 0) + return 0; + + t += len; + + while(*t && strchr(delims, *t)) + { + found = 1; + t++; + } + + return (!*t || found) ? t - line : 0; +} + +int is_last_word(const char* line, const char* word, int len) +{ + const char* t; + + ASSERT(line); + ASSERT(word); + ASSERT(len > 0); + + t = line + strlen(line); + + while(t > line && isspace(*(t - 1))) + --t; + + if(t - len < line) + return 0; + + return strncasecmp(t - len, word, len) == 0; +} + +int is_blank_line(const char* line) +{ + /* Small optimization */ + if(!*line) + return 1; + + while(*line && isspace(*line)) + line++; + + return *line == 0; +} + +char* trim_start(const char* data) +{ + while(*data && isspace(*data)) + ++data; + return (char*)data; +} + +char* trim_end(char* data) +{ + char* t = data + strlen(data); + + while(t > data && isspace(*(t - 1))) + { + t--; + *t = 0; + } + + return data; +} + +char* trim_space(char* data) +{ + data = (char*)trim_start(data); + return trim_end(data); +} + +/* String to bool helper function */ +int strtob(const char* str) +{ + if(strcasecmp(str, "0") == 0 || + strcasecmp(str, "no") == 0 || + strcasecmp(str, "false") == 0 || + strcasecmp(str, "f") == 0 || + strcasecmp(str, "off") == 0) + return 0; + + if(strcasecmp(str, "1") == 0 || + strcasecmp(str, "yes") == 0 || + strcasecmp(str, "true") == 0 || + strcasecmp(str, "t") == 0 || + strcasecmp(str, "on") == 0) + return 1; + + return -1; +} diff --git a/common/stringx.h b/common/stringx.h new file mode 100644 index 0000000..fb98d02 --- /dev/null +++ b/common/stringx.h @@ -0,0 +1,53 @@ +/* + * 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 + * Stef Walter <stef@memberwebs.com> + * + */ + +#ifndef __STRINGX_H__ +#define __STRINGX_H__ + +int check_first_word(const char* line, const char* word, int len, char* delims); +int is_first_word(const char* line, const char* word, int len); +int is_last_word(const char* line, const char* word, int len); +int is_blank_line(const char* line); + +char* trim_start(const char* data); +char* trim_end(char* data); +char* trim_space(char* data); + +int strtob(const char* str); + +#endif /* __STRINGX_H__ */ diff --git a/common/usuals.h b/common/usuals.h new file mode 100644 index 0000000..a32d6e9 --- /dev/null +++ b/common/usuals.h @@ -0,0 +1,78 @@ +/* + * 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 + * Stef Walter <stef@memberwebs.com> + * + */ + +#ifndef __USUALS_H__ +#define __USUALS_H__ + +#include <sys/types.h> + +#include "config.h" + +#include <stdio.h> +#include <stdlib.h> +#include <errno.h> +#include <string.h> + +#include "compat.h" + +#ifndef NULL +#define NULL 0 +#endif + +#ifndef max +#define max(a,b) (((a) > (b)) ? (a) : (b)) +#endif + +#ifndef min +#define min(a,b) (((a) < (b)) ? (a) : (b)) +#endif + +#define countof(x) (sizeof(x) / sizeof(x[0])) + +#ifdef _DEBUG + #include "assert.h" + #define ASSERT(x) assert(x) +#else + #define ASSERT(x) +#endif + +#define KL(s) ((sizeof(s) - 1) / sizeof(char)) +#define RETURN(x) { ret = (x); goto cleanup; } + + +#endif /* __USUALS_H__ */ |