summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Makefile.am7
-rw-r--r--src/clamsmtpd.8207
-rw-r--r--src/clamsmtpd.c247
-rw-r--r--src/clamsmtpd.h57
-rw-r--r--src/clio.c12
-rw-r--r--src/clstate.c325
-rw-r--r--src/usuals.h2
-rw-r--r--src/util.c14
-rw-r--r--src/util.h3
9 files changed, 504 insertions, 370 deletions
diff --git a/src/Makefile.am b/src/Makefile.am
index 5b18c6b..3c8deb6 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -2,11 +2,8 @@
sbin_PROGRAMS = clamsmtpd
clamsmtpd_SOURCES = clamsmtpd.c clamsmtpd.h util.c util.h sock_any.h sock_any.c \
- compat.c compat.h usuals.h clio.c
-
-man_MANS = clamsmtpd.8
-EXTRA_DIST = $(man_MANS)
+ compat.c compat.h usuals.h clio.c clstate.c
all-local:
@echo "NOTE: Ignore any warnings about mktemp(). It's used safely in this case."
- @echo \ No newline at end of file
+ @echo
diff --git a/src/clamsmtpd.8 b/src/clamsmtpd.8
deleted file mode 100644
index 75a1cae..0000000
--- a/src/clamsmtpd.8
+++ /dev/null
@@ -1,207 +0,0 @@
-.\"
-.\" Copyright (c) 2004, Nate Nielsen
-.\" 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
-.\" Nate Nielsen <nielsen@memberwebs.com>
-.\"
-.Dd July, 2004
-.Dt clamsmtpd 8
-.Os clamsmtp
-.Sh NAME
-.Nm clamsmtpd
-.Nd an SMTP server for scanning viruses via clamd
-.Sh SYNOPSIS
-.Nm
-.Op Fl bq
-.Op Fl c Ar clamaddr
-.Op Fl d Ar level
-.Op Fl D Ar tmpdir
-.Op Fl h Ar header
-.Op Fl l Ar listenaddr
-.Op Fl m Ar maxconn
-.Op Fl p Ar pidfile
-.Op Fl r
-.Op Fl t Ar timeout
-.Ar serveraddr
-.Nm
-.Fl v
-.Sh DESCRIPTION
-.Nm
-is an SMTP filter that allows you to check for viruses using the ClamAV
-anti-virus software. It accepts SMTP connections and forwards the SMTP commands
-and responses to another SMTP server.
-.Pp
-The DATA email body is intercepted and scanned before forwarding. By default email
-with viruses are dropped silently and logged without any additional action taken.
-.Pp
-.Nm
-aims to be lightweight and simple rather than have a myriad of options. Your
-basic usage would look like the following (Be sure to see the SECURITY section
-below):
-.Pp
-.Dl clamsmtpd -c /path/to/clam.sock mysmtp.com:25
-.Pp
-The above command would start
-.Nm
-listening on port 10025 (the default) and forward email to mysmtp.com on port 25.
-It also specifies the socket where
-.Xr clamd 8
-is listening for connections.
-.Sh OPTIONS
-The options are as follows:
-.Bl -tag -width Fl
-.It Fl b
-When this flag is set
-.Nm
-actively rejects messages with viruses. This may cause the sender to receive
-a message back notifying them of the virus. In most cases this is not a good
-idea since many viruses spoof sender addresses.
-.It Fl c
-.Ar clamaddr
-specifies the address to connect to
-.XR clamd 8
-on. See syntax of addresses below.
-[Default:
-.Pa /var/run/clamav/clamd
-]
-.It Fl d
-Don't detach from the console and run as a daemon. In addition the
-.Ar level
-argument specifies what level of error messages to display. 0 being
-the least, 4 the most.
-.It Fl D
-.Ar tmpdir
-is the directory to write temp files too. This directory needs to be
-accessible to both
-.Xr clamd 8
-and
-.Nm
-[Default:
-.Pa /tmp
-]
-.It Fl h
-.Ar header
-is a header to add to scanned messages. Add a blank argument to not add
-a header. [Default: 'X-AV-Checked: ClamAV using ClamSMTP']
-.It Fl l
-.Ar listenaddr
-is the address and port to listen for SMTP connections on. See syntax of
-addresses below. [Default: port 10025 on all local IP addresses]
-.It Fl m
-.Ar maxconn
-specifies the maximum number of connections to accept at once.
-[Default: 64]
-.It Fl p
-This option causes
-.Nm
-to write a file with the daemon's process id, which can be used to stop the
-daemon.
-.Ar pidfile
-is the location of the file.
-.It Fl q
-Quarantine files that contain viruses by leaving them in the
-.Ar tmpdir
-directory. The file names look like this (where X is a random
-character or number):
-.Pa virus.XXXXXX
-.It Fl t
-.Ar timeout
-is the number of seconds to wait while reading data from network connections.
-[Default: 180 seconds]
-.It Fl v
-Prints the clamsmtp version number and exits.
-.It serveraddr
-The address of the SMTP server to send email to once it's been scanned. This
-option must be specified. See syntax of addreses below.
-.El
-.Sh LOGGING
-.Nm
-logs to
-.Xr syslogd
-by default under the 'mail' facility. You can also output logs to the console
-using the
-.Fl d
-option.
-.Sh LOOPBACK FEATURE
-In some cases it's advantagous to consolidate the virus scanning and filtering
-for several mail servers on one machine.
-.Nm
-allows this by providing a loopback feature to connect back to the IP that an
-SMTP connection comes in from.
-.Pp
-To use this feature specify only a port number (no IP address) for the
-.Ar serveraddr
-in which case
-.Nm
-will pass the email back to the said port on the incoming IP address.
-.Pp
-Make sure the
-.Ar maxconn
-setting is set high enough to handle the mail from all the servers without refusing
-connections.
-.Sh SECURITY
-There's no reason to run this daemon as root. It is meant as a filter and should
-listen on a high TCP port. It's probably a good idea to run it using the same
-user as the
-.Xr clamd 8
-daemon. This way the temporary files it writes are accessible to
-.Xr clamd 8
-.Pp
-Care should be taken with the directory that
-.Nm
-writes its temporary files to. In order to be secure, it should not be a world
-writeable location. Specify the directory using the
-.Fl t
-option.
-.Pp
-.Nm
-should probably not be run on a publicly accessible IP address or without a
-firewall. This is especially true if the loopback feature is used (see above).
-.Sh ADDRESSES
-Addresses can be specified in multiple formats:
-.Bl -bullet
-.It
-Unix local addresses can be specified by specifying their full path.
-(ie: '/var/run/clamav/clamd').
-.It
-IP addresses can be specified using dotted notation with a colon before
-the port number (ie: '127.0.0.1:3310').
-.It
-IPv6 addresses can be specified using bracketted notation with a colon
-before the port number (ie: '[::1]:3310')
-.El
-.Sh SEE ALSO
-.Xr clamd 8 ,
-.Xr clamdscan 1
-.Sh AUTHOR
-.An Nate Nielsen Aq nielsen@memberwebs.com
diff --git a/src/clamsmtpd.c b/src/clamsmtpd.c
index e68bdf1..4ffb003 100644
--- a/src/clamsmtpd.c
+++ b/src/clamsmtpd.c
@@ -43,7 +43,6 @@
#include <sys/stat.h>
#include <ctype.h>
-#include <paths.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
@@ -51,7 +50,6 @@
#include <signal.h>
#include <errno.h>
#include <err.h>
-#include <pthread.h>
#include "usuals.h"
#include "compat.h"
@@ -74,8 +72,6 @@ clamsmtp_thread_t;
* STRINGS
*/
-#define KL(s) ((sizeof(s) - 1) / sizeof(char))
-
#define CRLF "\r\n"
#define SMTP_TOOLONG "500 Line too long" CRLF
@@ -126,44 +122,14 @@ clamsmtp_thread_t;
#define CLAM_CONNECT "SESSION\nPING\n"
#define CLAM_DISCONNECT "END\n"
-/* -----------------------------------------------------------------------
- * DEFAULT SETTINGS
- */
-
-#define DEFAULT_SOCKET "10025"
-#define DEFAULT_PORT 10025
-#define DEFAULT_CLAMAV "/var/run/clamav/clamd"
-#define DEFAULT_MAXTHREADS 64
-#define DEFAULT_TIMEOUT 180
-#define DEFAULT_HEADER "X-AV-Checked: ClamAV using ClamSMTP"
+#define DEFAULT_CONFIG CONF_PREFIX "/httpauthd.conf"
/* -----------------------------------------------------------------------
* GLOBALS
*/
-int g_daemonized = 0; /* Currently running as a daemon */
-int g_debuglevel = LOG_ERR; /* what gets logged to console */
-int g_maxthreads = DEFAULT_MAXTHREADS; /* The maximum number of threads */
-struct timeval g_timeout = { DEFAULT_TIMEOUT, 0 };
-
-struct sockaddr_any g_outaddr; /* The outgoing address */
-const char* g_outname = NULL;
-struct sockaddr_any g_clamaddr; /* Address for connecting to clamd */
-const char* g_clamname = DEFAULT_CLAMAV;
-
-char* g_header = DEFAULT_HEADER; /* The header to add to email */
-const char* g_directory = _PATH_TMP; /* The directory for temp files */
+clstate_t g_state; /* The state and configuration of the daemon */
unsigned int g_unique_id = 0x00100000; /* For connection ids */
-int g_bounce = 0; /* Send back a reject line */
-int g_quarantine = 0; /* Leave virus files in temp dir */
-int g_debugfiles = 0; /* Leave all files in temp dir */
-
-/* For main loop and signal handlers */
-int g_quit = 0;
-
-/* The main mutex and condition variables */
-pthread_mutex_t g_mutex;
-pthread_mutexattr_t g_mutexattr;
/* -----------------------------------------------------------------------
@@ -172,7 +138,7 @@ pthread_mutexattr_t g_mutexattr;
static void usage();
static void on_quit(int signal);
-static void pid_file(const char* pid, int write);
+static void pid_file(int write);
static void connection_loop(int sock);
static void* thread_main(void* arg);
static int smtp_passthru(clamsmtp_context_t* ctx);
@@ -195,86 +161,90 @@ static void read_junk(clamsmtp_context_t* ctx, int fd);
int main(int argc, char* argv[])
{
- const char* listensock = DEFAULT_SOCKET;
- struct sockaddr_any addr;
- char* pidfile = NULL;
- int daemonize = 1;
+ const char* configfile = DEFAULT_CONFIG;
+ int warnargs = 0;
int sock;
int true = 1;
int ch = 0;
char* t;
+ clstate_init(&g_state);
+
/* Parse the arguments nicely */
- while((ch = getopt(argc, argv, "bc:d:D:h:l:m:p:qt:vX")) != -1)
+ while((ch = getopt(argc, argv, "bc:d:D:h:l:m:p:qt:v")) != -1)
{
switch(ch)
{
/* Actively reject messages */
case 'b':
- g_bounce = 1;
+ g_state.bounce = 1;
+ warnargs = 1;
break;
/* Change the CLAM socket */
case 'c':
- g_clamname = optarg;
+ g_state.clamname = optarg;
+ warnargs = 1;
break;
/* Don't daemonize */
case 'd':
- daemonize = 0;
- g_debuglevel = strtol(optarg, &t, 10);
- if(*t || g_debuglevel > 4)
+ g_state.debug_level = strtol(optarg, &t, 10);
+ if(*t) /* parse error */
errx(1, "invalid debug log level");
- g_debuglevel += LOG_ERR;
+ g_state.debug_level += LOG_ERR;
break;
/* The directory for the files */
case 'D':
- g_directory = optarg;
+ g_state.directory = optarg;
+ warnargs = 1;
+ break;
+
+ /* The configuration file */
+ case 'f':
+ configfile = optarg;
break;
/* The header to add */
case 'h':
if(strlen(optarg) == 0)
- g_header = NULL;
+ g_state.header = NULL;
else
- {
- g_header = optarg;
-
- /* Trim off any ending newline chars */
- t = g_header + strlen(g_header);
- while(t > g_header && (*(t - 1) == '\r' || *(t - 1) == '\n'))
- *(--t) = 0;
- }
+ g_state.header = optarg;
+ warnargs = 1;
break;
/* Change our listening port */
case 'l':
- listensock = optarg;
+ g_state.listenname = optarg;
+ warnargs = 1;
break;
/* The maximum number of threads */
case 'm':
- g_maxthreads = strtol(optarg, &t, 10);
- if(*t || g_maxthreads <= 1 || g_maxthreads >= 1024)
- errx(1, "invalid max threads (must be between 1 and 1024");
+ g_state.max_threads = strtol(optarg, &t, 10);
+ if(*t) /* parse error */
+ errx(1, "invalid max threads");
+ warnargs = 1;
break;
/* Write out a pid file */
case 'p':
- pidfile = optarg;
+ g_state.pidfile = optarg;
break;
/* The timeout */
case 't':
- g_timeout.tv_sec = strtol(optarg, &t, 10);
- if(*t || g_timeout.tv_sec <= 0)
- errx(1, "invalid timeout: %s", optarg);
+ g_state.timeout.tv_sec = strtol(optarg, &t, 10);
+ if(*t) /* parse error */
+ errx(1, "invalid timeout");
+ warnargs = 1;
break;
/* Leave virus files in directory */
case 'q':
- g_quarantine = 1;
+ g_state.quarantine = 1;
break;
/* Print version number */
@@ -285,7 +255,8 @@ int main(int argc, char* argv[])
/* Leave all files in the tmp directory */
case 'X':
- g_debugfiles = 1;
+ g_state.debug_files = 1;
+ warnargs = 1;
break;
/* Usage information */
@@ -296,25 +267,33 @@ int main(int argc, char* argv[])
}
}
+ if(warnargs);
+ warnx("please use configuration file instead of command-line flags: %s", configfile);
+
argc -= optind;
argv += optind;
- if(argc != 1)
+ if(argc > 1)
usage();
+ if(argc == 1)
+ g_state.outname = argv[0];
+
+ /* Now parse the configuration file */
+ if(clstate_parse_config(&g_state, configfile) == -1)
+ {
+ /* Only error when it was forced */
+ if(configfile != DEFAULT_CONFIG)
+ err(1, "couldn't open config file: %s", configfile);
+ else
+ warnx("default configuration file not found: %s", configfile);
+ }
- g_outname = argv[0];
+ clstate_validate(&g_state);
messagex(NULL, LOG_DEBUG, "starting up...");
- /* Parse all the addresses */
- if(sock_any_pton(listensock, &addr, SANY_OPT_DEFANY | SANY_OPT_DEFPORT(DEFAULT_PORT)) == -1)
- errx(1, "invalid listen socket name or ip: %s", listensock);
- if(sock_any_pton(g_outname, &g_outaddr, SANY_OPT_DEFPORT(25)) == -1)
- errx(1, "invalid connect socket name or ip: %s", g_outname);
- if(sock_any_pton(g_clamname, &g_clamaddr, SANY_OPT_DEFLOCAL) == -1)
- errx(1, "invalid clam socket name: %s", g_clamname);
-
- if(daemonize)
+ /* When set to this we daemonize */
+ if(g_state.debug_level == -1)
{
/* Fork a daemon nicely here */
if(daemon(0, 0) == -1)
@@ -324,51 +303,61 @@ int main(int argc, char* argv[])
}
messagex(NULL, LOG_DEBUG, "running as a daemon");
- g_daemonized = 1;
+ g_state.daemonized = 1;
/* Open the system log */
openlog("clamsmtpd", 0, LOG_MAIL);
}
/* Create the socket */
- sock = socket(SANY_TYPE(addr), SOCK_STREAM, 0);
+ sock = socket(SANY_TYPE(g_state.listenaddr), SOCK_STREAM, 0);
if(sock < 0)
- err(1, "couldn't open socket");
+ {
+ message(NULL, LOG_CRIT, "couldn't open socket");
+ exit(1);
+ }
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (void *)&true, sizeof(true));
/* Unlink the socket file if it exists */
- if(SANY_TYPE(addr) == AF_UNIX)
- unlink(listensock);
+ if(SANY_TYPE(g_state.listenaddr) == AF_UNIX)
+ unlink(g_state.listenname);
- if(bind(sock, &SANY_ADDR(addr), SANY_LEN(addr)) != 0)
- err(1, "couldn't bind to address: %s", listensock);
+ if(bind(sock, &SANY_ADDR(g_state.listenaddr), SANY_LEN(g_state.listenaddr)) != 0)
+ {
+ message(NULL, LOG_CRIT, "couldn't bind to address: %s", g_state.listenname);
+ exit(1);
+ }
/* Let 5 connections queue up */
if(listen(sock, 5) != 0)
- err(1, "couldn't listen on socket");
+ {
+ message(NULL, LOG_CRIT, "couldn't listen on socket");
+ exit(1);
+ }
- messagex(NULL, LOG_DEBUG, "created socket: %s", listensock);
+ messagex(NULL, LOG_DEBUG, "created socket: %s", g_state.listenname);
/* Handle some signals */
signal(SIGPIPE, SIG_IGN);
- signal(SIGHUP, SIG_IGN);
+ signal(SIGHUP, SIG_IGN);
signal(SIGINT, on_quit);
signal(SIGTERM, on_quit);
siginterrupt(SIGINT, 1);
siginterrupt(SIGTERM, 1);
- if(pidfile)
- pid_file(pidfile, 1);
+ if(g_state.pidfile)
+ pid_file(1);
messagex(NULL, LOG_DEBUG, "accepting connections");
connection_loop(sock);
- if(pidfile)
- pid_file(pidfile, 0);
+ if(g_state.pidfile)
+ pid_file(0);
+ clstate_cleanup(&g_state);
messagex(NULL, LOG_DEBUG, "stopped");
return 0;
@@ -376,45 +365,43 @@ int main(int argc, char* argv[])
static void on_quit(int signal)
{
- g_quit = 1;
-
+ g_state.quit = 1;
/* fprintf(stderr, "clamsmtpd: got signal to quit\n"); */
}
static void usage()
{
- fprintf(stderr, "usage: clamsmtpd [-bq] [-c clamaddr] [-d debuglevel] [-D tmpdir] [-h header] "
- "[-l listenaddr] [-m maxconn] [-p pidfile] [-t timeout] serveraddr\n");
+ fprintf(stderr, "usage: clamsmtpd [-d debuglevel] [-f configfile] \n");
fprintf(stderr, " clamsmtpd -v\n");
exit(2);
}
-static void pid_file(const char* pidfile, int write)
+static void pid_file(int write)
{
if(write)
{
- FILE* f = fopen(pidfile, "w");
+ FILE* f = fopen(g_state.pidfile, "w");
if(f == NULL)
{
- message(NULL, LOG_ERR, "couldn't open pid file: %s", pidfile);
+ message(NULL, LOG_ERR, "couldn't open pid file: %s", g_state.pidfile);
}
else
{
fprintf(f, "%d\n", (int)getpid());
if(ferror(f))
- message(NULL, LOG_ERR, "couldn't write to pid file: %s", pidfile);
+ message(NULL, LOG_ERR, "couldn't write to pid file: %s", g_state.pidfile);
fclose(f);
}
- messagex(NULL, LOG_DEBUG, "wrote pid file: %s", pidfile);
+ messagex(NULL, LOG_DEBUG, "wrote pid file: %s", g_state.pidfile);
}
else
{
- unlink(pidfile);
- messagex(NULL, LOG_DEBUG, "removed pid file: %s", pidfile);
+ unlink(g_state.pidfile);
+ messagex(NULL, LOG_DEBUG, "removed pid file: %s", g_state.pidfile);
}
}
@@ -429,18 +416,12 @@ static void connection_loop(int sock)
int fd, i, x, r;
/* Create the thread buffers */
- threads = (clamsmtp_thread_t*)calloc(g_maxthreads, sizeof(clamsmtp_thread_t));
+ threads = (clamsmtp_thread_t*)calloc(g_state.max_threads, sizeof(clamsmtp_thread_t));
if(!threads)
errx(1, "out of memory");
- /* Create the main mutex and condition variable */
- if(pthread_mutexattr_init(&g_mutexattr) != 0 ||
- pthread_mutexattr_settype(&g_mutexattr, MUTEX_TYPE) ||
- pthread_mutex_init(&g_mutex, &g_mutexattr) != 0)
- errx(1, "threading problem. can't create mutex or condition var");
-
/* Now loop and accept the connections */
- while(!g_quit)
+ while(!g_state.quit)
{
fd = accept(sock, NULL, NULL);
if(fd == -1)
@@ -457,23 +438,23 @@ static void connection_loop(int sock)
default:
message(NULL, LOG_ERR, "couldn't accept a connection");
- g_quit = 1;
+ g_state.quit = 1;
break;
};
- if(g_quit)
+ if(g_state.quit)
break;
continue;
}
/* Set timeouts on client */
- if(setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &g_timeout, sizeof(g_timeout)) < 0 ||
- setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &g_timeout, sizeof(g_timeout)) < 0)
+ 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)
message(NULL, LOG_WARNING, "couldn't set timeouts on incoming connection");
/* Look for thread and also clean up others */
- for(i = 0; i < g_maxthreads; i++)
+ for(i = 0; i < g_state.max_threads; i++)
{
/* Find a thread to run or clean up old threads */
if(threads[i].tid != 0)
@@ -507,7 +488,7 @@ static void connection_loop(int sock)
{
errno = r;
message(NULL, LOG_ERR, "couldn't create thread");
- g_quit = 1;
+ g_state.quit = 1;
break;
}
@@ -520,7 +501,7 @@ static void connection_loop(int sock)
/* Check to make sure we have a thread */
if(fd != -1)
{
- messagex(NULL, LOG_ERR, "too many connections open (max %d). sent 554 response", g_maxthreads);
+ messagex(NULL, LOG_ERR, "too many connections open (max %d). sent 554 response", g_state.max_threads);
write(fd, SMTP_STARTBUSY, KL(SMTP_STARTBUSY));
shutdown(fd, SHUT_RDWR);
@@ -532,7 +513,7 @@ static void connection_loop(int sock)
messagex(NULL, LOG_DEBUG, "waiting for threads to quit");
/* Quit all threads here */
- for(i = 0; i < g_maxthreads; i++)
+ for(i = 0; i < g_state.max_threads; i++)
{
/* Clean up quit threads */
if(threads[i].tid != 0)
@@ -551,10 +532,6 @@ static void connection_loop(int sock)
pthread_join(threads[i].tid, NULL);
}
}
-
- /* Close the mutex */
- pthread_mutex_destroy(&g_mutex);
- pthread_mutexattr_destroy(&g_mutexattr);
}
static void* thread_main(void* arg)
@@ -616,15 +593,15 @@ static void* thread_main(void* arg)
/* Create the server connection address */
- outaddr = &g_outaddr;
- outname = g_outname;
+ outaddr = &(g_state.outaddr);
+ outname = g_state.outname;
if(SANY_TYPE(*outaddr) == AF_INET &&
outaddr->s.in.sin_addr.s_addr == 0)
{
/* Use the incoming IP as the default */
in_addr_t in = addr.s.in.sin_addr.s_addr;
- memcpy(&addr, &g_outaddr, sizeof(addr));
+ memcpy(&addr, &(g_state.outaddr), sizeof(addr));
addr.s.in.sin_addr.s_addr = in;
outaddr = &addr;
@@ -954,7 +931,7 @@ static int avcheck_data(clamsmtp_context_t* ctx, char* logline)
int havefile = 0;
int r, ret = 0;
- strlcpy(buf, g_directory, MAXPATHLEN);
+ strlcpy(buf, g_state.directory, MAXPATHLEN);
strlcat(buf, "/clamsmtpd.XXXXXX", MAXPATHLEN);
/* transfer_to_file deletes the temp file on failure */
@@ -993,7 +970,7 @@ static int avcheck_data(clamsmtp_context_t* ctx, char* logline)
*/
case 1:
if(clio_write_data(ctx, &(ctx->client),
- g_bounce ? SMTP_DATAVIRUS : SMTP_DATAVIRUSOK) == -1)
+ g_state.bounce ? SMTP_DATAVIRUS : SMTP_DATAVIRUSOK) == -1)
RETURN(-1);
/* Any special post operation actions on the virus */
@@ -1006,7 +983,7 @@ static int avcheck_data(clamsmtp_context_t* ctx, char* logline)
};
cleanup:
- if(havefile && !g_debugfiles)
+ if(havefile && !g_state.debug_files)
{
messagex(ctx, LOG_DEBUG, "deleting temporary file: %s", buf);
unlink(buf);
@@ -1089,7 +1066,7 @@ static int connect_clam(clamsmtp_context_t* ctx)
ASSERT(ctx);
ASSERT(!clio_valid(&(ctx->clam)));
- if(clio_connect(ctx, &(ctx->clam), &g_clamaddr, g_clamname) == -1)
+ if(clio_connect(ctx, &(ctx->clam), &g_state.clamaddr, g_state.clamname) == -1)
RETURN(-1);
read_junk(ctx, ctx->clam.fd);
@@ -1193,10 +1170,10 @@ static int quarantine_virus(clamsmtp_context_t* ctx, char* tempname)
char buf[MAXPATHLEN];
char* t;
- if(!g_quarantine)
+ if(!g_state.quarantine)
return 0;
- strlcpy(buf, g_directory, MAXPATHLEN);
+ strlcpy(buf, g_state.directory, MAXPATHLEN);
strlcat(buf, "/virus.", MAXPATHLEN);
/* Points to null terminator */
@@ -1328,7 +1305,7 @@ static int transfer_from_file(clamsmtp_context_t* ctx, const char* filename)
while(fgets(ctx->line, LINE_LENGTH, file) != NULL)
{
- if(g_header && !header)
+ if(g_state.header && !header)
{
/*
* The first blank line we see means the headers are done.
@@ -1336,7 +1313,7 @@ static int transfer_from_file(clamsmtp_context_t* ctx, const char* filename)
*/
if(is_blank_line(ctx->line))
{
- if(clio_write_data_raw(ctx, &(ctx->server), (char*)g_header, strlen(g_header)) == -1 ||
+ if(clio_write_data_raw(ctx, &(ctx->server), (char*)g_state.header, strlen(g_state.header)) == -1 ||
clio_write_data_raw(ctx, &(ctx->server), CRLF, KL(CRLF)) == -1)
RETURN(-1);
diff --git a/src/clamsmtpd.h b/src/clamsmtpd.h
index ca3df37..deb2cfb 100644
--- a/src/clamsmtpd.h
+++ b/src/clamsmtpd.h
@@ -39,6 +39,10 @@
#ifndef __CLAMSMTPD_H__
#define __CLAMSMTPD_H__
+#include <sock_any.h>
+
+/* IO Buffers see clio.c ---------------------------------------------------- */
+
#define BUF_LEN 256
typedef struct clio
@@ -50,6 +54,8 @@ typedef struct clio
}
clio_t;
+/* The main context --------------------------------------------------------- */
+
/*
* A generous maximum line length. It needs to be longer than
* a full path on this system can be, because we pass the file
@@ -75,17 +81,12 @@ typedef struct clamsmtp_context
}
clamsmtp_context_t;
-extern int g_daemonized; /* Currently running as a daemon */
-extern int g_debuglevel; /* what gets logged to console */
-extern pthread_mutex_t g_mutex; /* The main mutex */
-extern struct timeval g_timeout;
-extern int g_quit;
-
-struct sockaddr_any;
#define LINE_TOO_LONG(ctx) ((ctx)->linelen >= (LINE_LENGTH - 2))
#define RETURN(x) { ret = x; goto cleanup; }
+/* Implemented in clio.c ---------------------------------------------------- */
+
#define CLIO_TRIM 0x00000001
#define CLIO_DISCARD 0x00000002
#define CLIO_QUIET 0x00000004
@@ -99,4 +100,46 @@ int clio_read_line(clamsmtp_context_t* ctx, clio_t* io, int trim);
int clio_write_data(clamsmtp_context_t* ctx, clio_t* io, const char* data);
int clio_write_data_raw(clamsmtp_context_t* ctx, clio_t* io, unsigned char* buf, int len);
+
+/* Implemented in clstate.c ------------------------------------------------ */
+
+typedef struct clstate
+{
+ /* 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 */
+
+ struct sockaddr_any outaddr; /* The outgoing address */
+ const char* outname;
+ struct sockaddr_any clamaddr; /* Address for connecting to clamd */
+ const char* clamname;
+ struct sockaddr_any listenaddr; /* Address to listen on */
+ const char* listenname;
+
+ const char* header; /* The header to add to email */
+ const char* directory; /* The directory for temp files */
+ const char* pidfile; /* The process id file */
+ int bounce; /* Send back a reject line */
+ int quarantine; /* Leave virus files in temp dir */
+ int debug_files; /* Leave all files in temp dir */
+
+ /* State --------------------------------- */
+ int daemonized; /* Whether process is daemonized or not */
+ pthread_mutex_t mutex; /* The main mutex */
+ int quit; /* Quit the process */
+
+ /* Internal Use ------------------------- */
+ char* _p;
+ pthread_mutexattr_t _mtxattr;
+}
+clstate_t;
+
+extern clstate_t g_state;
+
+void clstate_init(clstate_t* state);
+int clstate_parse_config(clstate_t* state, const char* configfile);
+void clstate_validate(clstate_t* state);
+void clstate_cleanup(clstate_t* state);
+
#endif /* __CLAMSMTPD_H__ */
diff --git a/src/clio.c b/src/clio.c
index 1794118..a2694bb 100644
--- a/src/clio.c
+++ b/src/clio.c
@@ -90,7 +90,7 @@ static void log_io_data(clamsmtp_context_t* ctx, clio_t* io, const char* data, i
memcpy(buf, data, len);
buf[len] = 0;
- messagex(ctx, LOG_DEBUG, "%s%s%s", GET_IO_NAME(io),
+ messagex(0, LOG_DEBUG, "%s%s%s", GET_IO_NAME(io),
read ? " < " : " > ", buf);
data += pos;
@@ -116,8 +116,8 @@ int clio_connect(clamsmtp_context_t* ctx, clio_t* io, struct sockaddr_any* sany,
if((io->fd = socket(SANY_TYPE(*sany), SOCK_STREAM, 0)) == -1)
RETURN(-1);
- if(setsockopt(io->fd, SOL_SOCKET, SO_RCVTIMEO, &g_timeout, sizeof(g_timeout)) == -1 ||
- setsockopt(io->fd, SOL_SOCKET, SO_SNDTIMEO, &g_timeout, sizeof(g_timeout)) == -1)
+ if(setsockopt(io->fd, SOL_SOCKET, SO_RCVTIMEO, &g_state.timeout, sizeof(g_state.timeout)) == -1 ||
+ setsockopt(io->fd, SOL_SOCKET, SO_SNDTIMEO, &g_state.timeout, sizeof(g_state.timeout)) == -1)
messagex(ctx, LOG_WARNING, "couldn't set timeouts on connection");
if(connect(io->fd, &SANY_ADDR(*sany), SANY_LEN(*sany)) == -1)
@@ -183,7 +183,7 @@ int clio_select(clamsmtp_context_t* ctx, clio_t** io)
/* Select on the above */
- switch(select(FD_SETSIZE, &mask, NULL, NULL, &g_timeout))
+ switch(select(FD_SETSIZE, &mask, NULL, NULL, &g_state.timeout))
{
case 0:
messagex(ctx, LOG_ERR, "network operation timed out");
@@ -244,7 +244,7 @@ int clio_read_line(clamsmtp_context_t* ctx, clio_t* io, int opts)
if(errno == EINTR)
{
/* When the application is quiting */
- if(g_quit)
+ if(g_state.quit)
return -1;
/* For any other signal we go again */
@@ -380,7 +380,7 @@ int clio_write_data_raw(clamsmtp_context_t* ctx, clio_t* io, unsigned char* buf,
if(errno == EINTR)
{
/* When the application is quiting */
- if(g_quit)
+ if(g_state.quit)
return -1;
/* For any other signal we go again */
diff --git a/src/clstate.c b/src/clstate.c
new file mode 100644
index 0000000..1d5f2af
--- /dev/null
+++ b/src/clstate.c
@@ -0,0 +1,325 @@
+/*
+ * Copyright (c) 2004, Nate Nielsen
+ * 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
+ * Nate Nielsen <nielsen@memberwebs.com>
+ * Yamamoto Takao <takao@oakat.org>
+ */
+
+#include <paths.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <unistd.h>
+#include <err.h>
+#include <pthread.h>
+#include <syslog.h>
+
+#include "usuals.h"
+#include "compat.h"
+#include "clamsmtpd.h"
+#include "util.h"
+
+/* -----------------------------------------------------------------------
+ * DIRECTIONS FOR ADDING A CONFIGURATION OPTION
+ *
+ * - Add field to clstate_t structure in clamsmtpd.h
+ * - Add default and set in clstate_init (below)
+ * - Add config keyword (below)
+ * - Parsing of option in clstate_parse_config (below)
+ * - Validation of option in clstate_validate (below)
+ * - Document in the sample doc/clamsmtpd.conf
+ * - Document in doc/clamsmtpd.conf.5
+ */
+
+/* -----------------------------------------------------------------------
+ * DEFAULT SETTINGS
+ */
+
+#define DEFAULT_SOCKET "10025"
+#define DEFAULT_PORT 10025
+#define DEFAULT_CLAMAV "/var/run/clamav/clamd"
+#define DEFAULT_MAXTHREADS 64
+#define DEFAULT_TIMEOUT 180
+#define DEFAULT_HEADER "X-AV-Checked: ClamAV using ClamSMTP"
+
+/* -----------------------------------------------------------------------
+ * CONFIG KEYWORDS
+ */
+
+#define CFG_MAXTHREADS "MaxConnections"
+#define CFG_TIMEOUT "TimeOut"
+#define CFG_OUTADDR "OutAddress"
+#define CFG_LISTENADDR "Listen"
+#define CFG_CLAMADDR "ClamAddress"
+#define CFG_HEADER "ScanHeader"
+#define CFG_DIRECTORY "TempDirectory"
+#define CFG_BOUNCE "Bounce"
+#define CFG_QUARANTINE "Quarantine"
+#define CFG_DEBUGFILES "DebugFiles"
+#define CFG_PIDFILE "PidFile"
+
+/* The set of delimiters that can be present between config and value */
+#define CFG_DELIMS ": \t"
+
+/* -----------------------------------------------------------------------
+ * CODE
+ */
+
+/* String to bool helper function */
+static 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;
+}
+
+void clstate_init(clstate_t* state)
+{
+ ASSERT(state);
+ memset(state, 0, sizeof(*state));
+
+ /* Setup the defaults */
+ state->debug_level = -1;
+ state->max_threads = DEFAULT_MAXTHREADS;
+ state->timeout.tv_sec = DEFAULT_TIMEOUT;
+ state->clamname = DEFAULT_CLAMAV;
+ state->listenname = DEFAULT_SOCKET;
+ state->header = DEFAULT_HEADER;
+ state->directory = _PATH_TMP;
+
+ /* Create the main mutex and condition variable */
+ if(pthread_mutexattr_init(&(state->_mtxattr)) != 0 ||
+ pthread_mutexattr_settype(&(state->_mtxattr), MUTEX_TYPE) ||
+ pthread_mutex_init(&(state->mutex), &(state->_mtxattr)) != 0)
+ errx(1, "threading problem. can't create mutex or condition var");
+}
+
+int clstate_parse_config(clstate_t* state, const char* configfile)
+{
+ FILE* f = NULL;
+ long len;
+ int r;
+ char* p;
+ char* t;
+ char* n;
+
+ ASSERT(state);
+ ASSERT(configfile);
+ ASSERT(!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);
+ }
+
+ if(fseek(f, 0, SEEK_END) == -1 || (len = ftell(f)) == -1)
+ err(1, "couldn't seek config file: %s", configfile);
+
+ if((state->_p = (char*)malloc(len + 2)) == NULL)
+ errx(1, "out of memory");
+
+ if(fread(state->_p, 1, len, f) != len)
+ err(1, "couldn't read config file: %s", configfile);
+
+ fclose(f);
+ messagex(NULL, LOG_DEBUG, "successfully opened config file: %s", configfile);
+
+ /* Double null terminate the data */
+ p = state->_p;
+ p[len] = 0;
+ p[len + 1] = 0;
+
+ /* Now split string at new lines */
+ while((t = strchr(p, '\n')) != NULL)
+ {
+ *t = 0;
+ p = t + 1;
+ }
+
+ n = state->_p;
+
+ /* Go through lines and process them */
+ while(*n != 0)
+ {
+ p = n; /* Do this before trimming below */
+ n = p + strlen(p) + 1;
+
+ p = trim_space(p);
+
+ /* Comments and empty lines */
+ if(*p == 0 || *p == '#')
+ continue;
+
+ /* Save some code typing below */
+ #define PARSE(o) \
+ (r = check_first_word(p, (o), KL(o), CFG_DELIMS))
+ #define VAL \
+ (p + r)
+
+ /*
+ * Note that we don't validate here. If something's wrong
+ * set it to an invalid value.
+ */
+
+ if(PARSE(CFG_MAXTHREADS))
+ {
+ state->max_threads = strtol(VAL, &t, 10);
+ if(*t) /* parse failed */
+ state->max_threads = -1;
+ }
+
+ else if(PARSE(CFG_TIMEOUT))
+ {
+ state->timeout.tv_sec = strtol(VAL, &t, 10);
+ if(*t) /* parse failed */
+ state->timeout.tv_sec = -1;
+ }
+
+ else if(PARSE(CFG_OUTADDR))
+ state->outname = VAL;
+
+ else if(PARSE(CFG_LISTENADDR))
+ state->listenname = VAL;
+
+ else if(PARSE(CFG_CLAMADDR))
+ state->clamname = VAL;
+
+ else if(PARSE(CFG_HEADER))
+ state->header = VAL;
+
+ else if(PARSE(CFG_DIRECTORY))
+ state->directory = VAL;
+
+ else if(PARSE(CFG_PIDFILE))
+ state->pidfile = VAL;
+
+ else if(PARSE(CFG_BOUNCE))
+ state->bounce = strtob(VAL);
+
+ else if(PARSE(CFG_QUARANTINE))
+ state->quarantine = strtob(VAL);
+
+ else if(PARSE(CFG_DEBUGFILES))
+ state->debug_files = strtob(VAL);
+
+ /* Unrecognized option */
+ else
+ errx(2, "unrecognized line in config file: %s", p);
+
+ messagex(NULL, LOG_DEBUG, "successfully parsed line: %s", p);
+ }
+
+ return 0;
+}
+
+void clstate_validate(clstate_t* state)
+{
+ ASSERT(state);
+ messagex(NULL, LOG_DEBUG, "validating configuration options");
+
+ if(state->debug_level < -1 || state->debug_level > 4)
+ errx(2, "invalid debug log level (must be between 1 and 4)");
+
+ if(state->max_threads <= 1 || state->max_threads >= 1024)
+ errx(2, "invalid " CFG_MAXTHREADS " (must be between 1 and 1024)");
+
+ if(state->timeout.tv_sec <= 0)
+ errx(2, "invalid " CFG_TIMEOUT);
+
+ /* This option has no default, but is required */
+ if(state->outname == NULL)
+ errx(2, "no " CFG_OUTADDR " specified in config file.");
+
+ if(state->bounce == -1)
+ errx(2, "invalid value for " CFG_BOUNCE);
+ if(state->quarantine == -1)
+ errx(2, "invalid value for " CFG_QUARANTINE);
+ if(state->debug_files == -1)
+ errx(2, "invalid value for " CFG_DEBUGFILES);
+
+ /* Parse all the addresses */
+ if(sock_any_pton(state->listenname, &(state->listenaddr), SANY_OPT_DEFANY | SANY_OPT_DEFPORT(DEFAULT_PORT)) == -1)
+ errx(2, "invalid " CFG_LISTENADDR " socket name or ip: %s", state->listenname);
+ if(sock_any_pton(state->outname, &(state->outaddr), SANY_OPT_DEFPORT(25)) == -1)
+ errx(2, "invalid " CFG_OUTADDR " socket name or ip: %s", state->outname);
+ if(sock_any_pton(state->clamname, &(state->clamaddr), SANY_OPT_DEFLOCAL) == -1)
+ errx(2, "invalid " CFG_CLAMADDR " socket name: %s", state->clamname);
+
+ if(strlen(state->directory) == 0)
+ errx(2, "invalid " CFG_DIRECTORY);
+ if(state->pidfile && strlen(state->pidfile) == 0)
+ errx(2, "invalid " CFG_PIDFILE);
+
+ if(state->header)
+ {
+ /*
+ * This is for when it comes from the command-line.
+ * Once command line args are phased out this can be removed
+ */
+ state->header = (const char*)trim_space((char*)state->header);
+
+ if(strlen(state->header) == 0)
+ state->header = NULL;
+ }
+}
+
+void clstate_cleanup(clstate_t* state)
+{
+ if(state->_p)
+ {
+ free(state->_p);
+ memset(state, 0, sizeof(*state));
+ messagex(NULL, LOG_DEBUG, "freed configuration option memory");
+ }
+
+ /* Close the mutex */
+ pthread_mutex_destroy(&(state->mutex));
+ pthread_mutexattr_destroy(&(state->_mtxattr));
+}
diff --git a/src/usuals.h b/src/usuals.h
index 00410aa..385bcf9 100644
--- a/src/usuals.h
+++ b/src/usuals.h
@@ -71,4 +71,6 @@
#define ASSERT
#endif
+#define KL(s) ((sizeof(s) - 1) / sizeof(char))
+
#endif /* __USUALS_H__ */
diff --git a/src/util.c b/src/util.c
index 8b6a816..c0a46bc 100644
--- a/src/util.c
+++ b/src/util.c
@@ -67,14 +67,14 @@ static void vmessage(clamsmtp_context_t* ctx, int level, int err,
char* m;
int e = errno;
- if(g_daemonized)
+ if(g_state.daemonized)
{
if(level >= LOG_DEBUG)
return;
}
else
{
- if(g_debuglevel < level)
+ if(g_state.debug_level < level)
return;
}
@@ -102,7 +102,7 @@ static void vmessage(clamsmtp_context_t* ctx, int level, int err,
}
/* Either to syslog or stderr */
- if(g_daemonized)
+ if(g_state.daemonized)
vsyslog(level, msg, ap);
else
vwarnx(msg, ap);
@@ -244,16 +244,16 @@ void plock()
#endif
#ifdef _DEBUG
- r = pthread_mutex_trylock(&g_mutex);
+ r = pthread_mutex_trylock(&(g_state.mutex));
if(r == EBUSY)
{
wait = 1;
message(NULL, LOG_DEBUG, "thread will block: %d", pthread_self());
- r = pthread_mutex_lock(&g_mutex);
+ r = pthread_mutex_lock(&(g_state.mutex));
}
#else
- r = pthread_mutex_lock(&g_mutex);
+ r = pthread_mutex_lock(&(g_state.mutex));
#endif
@@ -273,7 +273,7 @@ void plock()
void punlock()
{
- int r = pthread_mutex_unlock(&g_mutex);
+ int r = pthread_mutex_unlock(&(g_state.mutex));
if(r != 0)
{
errno = r;
diff --git a/src/util.h b/src/util.h
index 8e6f2f4..37fa245 100644
--- a/src/util.h
+++ b/src/util.h
@@ -42,9 +42,6 @@
void messagex(clamsmtp_context_t* ctx, int level, const char* msg, ...);
void message(clamsmtp_context_t* ctx, int level, const char* msg, ...);
-void log_fd_data(clamsmtp_context_t* ctx, const char* data, int* fd, int read);
-void log_data(clamsmtp_context_t* ctx, const char* data, const char* prefix);
-
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);