/*
* netloop.c:
* Network event loop for tpop3d.
*
* Copyright (c) 2002 Chris Lightfoot. All rights reserved.
* Email: chris@ex-parrot.com; WWW: http://www.ex-parrot.com/~chris/
*
*/
static const char rcsid[] = "$Id: netloop.c,v 1.10 2003/11/24 19:58:28 chris Exp $";
#ifdef HAVE_CONFIG_H
#include "configuration.h"
#endif /* HAVE_CONFIG_H */
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <syslog.h>
#include <time.h>
#include <unistd.h>
#include <time.h>
#ifdef USE_TCP_WRAPPERS
# include <tcpd.h>
#endif
#include <sys/socket.h>
#include <sys/time.h>
#include "config.h"
#include "connection.h"
#include "listener.h"
#include "signals.h"
#include "stringmap.h"
#include "util.h"
/* The socket send buffer is set to this, so that we don't end up in a
* position that we send so much data that the client will not have received
* all of it before we time them out. */
#define MAX_DATA_IN_FLIGHT 8192
int max_running_children = 16; /* How many children may exist at once. */
volatile int num_running_children = 0; /* How many children are active. */
/* Variables representing the state of the server. */
int post_fork = 0; /* Is this a child handling a connection. */
connection this_child_connection; /* Stored here so that if a signal terminates the child, the mailspool will still get unlocked. */
int timeout_seconds = 30; /* How long a period of inactivity may elapse before a client is dropped. */
extern stringmap config; /* in main.c */
#ifdef USE_TCP_WRAPPERS
int allow_severity = LOG_INFO;
int deny_severity = LOG_NOTICE;
char *tcpwrappersname;
#endif
vector listeners; /* Listeners */
connection *connections; /* Active connections. */
size_t max_connections; /* Number of connection slots allocated. */
/*
* Theory of operation:
*
* The main loop is in net_loop, below; it calls listeners_ and
* connections_pre_select, then calls select, then calls listeners_ and
* connections_post_select. In the event that a server is forked to handle a
* client, fork_child is called. The global variables listeners and
* connections are used to handle this procedure.
*/
/* Because the main loop is single-threaded, under high load the server could
* alternate between accepting a large number of backlogged connections, and
* processing commands from and authenticating a large number of connected
* clients. In order to avoid this, we define a maximum time which the server
* may spend either (a) accepting new connections; or (b) processing commands
* from existing connections. (Obviously the same amount of work must be done
* in either case, but we can choose when to do it.) Effectively this should
* set how long any client could wait for a banner or response from the
* server. */
#define LATENCY 2 /* seconds */
/* find_free_connection
* Find a free connection slot. */
static connection *find_free_connection(void) {
connection *J;
for (J = connections; J < connections + max_connections; ++J)
if (!*J) return J;
return NULL;
}
/* remove_connection CONNECTION
* Remove CONNECTION from the list. */
static void remove_connection(connection c) {
connection *J;
for (J = connections; J < connections + max_connections; ++J)
if (*J == c) *J = NULL;
}
/* listeners_pre_select:
* Called before the main select(2) so listening sockets can be polled. */
static void listeners_pre_select(int *n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds) {
item *t;
vector_iterate(listeners, t) {
int s = ((listener)t->v)->s;
FD_SET(s, readfds);
if (s > *n) *n = s;
}
}
/* listeners_post_select:
* Called after the main select(2) to allow listening sockets to sort
* themselves out. */
static void listeners_post_select(fd_set *readfds, fd_set *writefds, fd_set *exceptfds) {
item *t;
vector_iterate(listeners, t) {
listener L = (listener)t->v;
if (FD_ISSET(L->s, readfds)) {
struct sockaddr_in sin, sinlocal;
size_t l = sizeof(sin);
int s, a = MAX_DATA_IN_FLIGHT;
time_t start;
time(&start);
errno = 0;
/* XXX socklen_t mess... */
while (time(NULL) < start + LATENCY && -1 != (s = accept(L->s, (struct sockaddr*)&sin, (int*)&l))) {
l = sizeof(sin);
if (-1 == getsockname(s, (struct sockaddr*)&sinlocal, (int*)&l)) {
log_print(LOG_ERR, "net_loop: getsockname: %m");
close(s);
}
#ifdef USE_TCP_WRAPPERS
else if (!hosts_ctl(tcpwrappersname, STRING_UNKNOWN, inet_ntoa(sin.sin_addr), STRING_UNKNOWN)) {
log_print(LOG_ERR, "net_loop: tcp_wrappers: connection from %s to local address %s:%d refused", inet_ntoa(sin.sin_addr), inet_ntoa(sinlocal.sin_addr), htons(sinlocal.sin_port));
close(s);
}
#endif
else if (setsockopt(s, SOL_SOCKET, SO_SNDBUF, &a, sizeof(a)) == -1) {
/* Set a small send buffer so that we get EAGAIN if the client
* isn't acking our data. */
log_print(LOG_ERR, "listeners_post_select: setsockopt: %m");
close(s);
} else if (fcntl(s, F_SETFL, O_NONBLOCK) == -1) {
/* Ensure that non-blocking operation is switched on, even if
* it isn't inherited. */
log_print(LOG_ERR, "listeners_post_select: fcntl(F_SETFL): %m");
close(s);
} else {
connection *J;
if (num_running_children >= max_running_children || !(J = find_free_connection())) {
shutdown(s, 2);
close(s);
log_print(LOG_WARNING, _("listeners_post_select: rejected connection from %s to local address %s:%d owing to high load"), inet_ntoa(sin.sin_addr), inet_ntoa(sinlocal.sin_addr), htons(sinlocal.sin_port));
} else {
/* Create connection object. */
if ((*J = connection_new(s, &sin, L)))
log_print(LOG_INFO, _("listeners_post_select: client %s: connected to local address %s:%d"), (*J)->idstr, inet_ntoa(sinlocal.sin_addr), htons(sinlocal.sin_port));
else
/* This could be really bad, but all we can do is log the failure. */
log_print(LOG_ERR, _("listeners_post_select: unable to set up connection from %s to local address %s:%d: %m"), inet_ntoa(sin.sin_addr), inet_ntoa(sinlocal.sin_addr), htons(sinlocal.sin_port));
}
}
}
if (errno != EAGAIN)
log_print(LOG_ERR, "net_loop: accept: %m");
}
}
}
/* connections_pre_select:
* Called before the main select(2) so connections can be polled. */
static void connections_pre_select(int *n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds) {
connection *J;
for (J = connections; J < connections + max_connections; ++J)
/* Don't add frozen connections to the select masks. */
if (*J && !connection_isfrozen(*J) && (*J)->cstate != closed)
(*J)->io->pre_select(*J, n, readfds, writefds, exceptfds);
}
/* fork_child CONNECTION
* Handle forking a child to handle CONNECTION after authentication. Returns 1
* on success or 0 on failure; the caller can determine whether they are now
* the child or the parent by testing the post_fork flag. On return in the
* parent the connection will have been destroyed and removed from the list;
* in the child, it will be the only remaining connection and all the
* listeners will have been destroyed. Optionally, the child can wait until
* any ONLOGIN handler has run in the parent, so that ONLOGIN can be used to
* implement POP3 server `bulletins' or similar behaviour. */
static int fork_child(connection c) {
connection *J;
item *t;
sigset_t chmask;
pid_t ch;
int childwait, pp[2];
/* Waiting for ONLOGIN handlers to complete is done using a pipe (when
* the only tool you have is a hammer...). The parent writes a byte to
* the pipe when the ONLOGIN handler is finished, and the child blocks
* reading from the pipe. NB do this before messing with the signal
* mask. */
if ((childwait = config_get_bool("onlogin-child-wait"))) {
if (pipe(pp) == -1) {
log_print(LOG_ERR, "fork_child: pipe: %m");
connection_sendresponse(c, 0, _("Everything was fine until now, but suddenly I realise I just can't go on. Sorry."));
return 0;
}
/* pp[0] is for reading, pp[1] is for writing */
}
/* We block SIGCHLD and SIGHUP during this function so as to avoid race
* conditions involving a child which exits immediately. */
sigemptyset(&chmask);
sigaddset(&chmask, SIGCHLD);
sigprocmask(SIG_BLOCK, &chmask, NULL);
post_fork = 1; /* This is right. See below. */
#ifdef MTRACE_DEBUGGING
muntrace(); /* Memory debugging on glibc systems. */
#endif /* MTRACE_DEBUGGING */
switch ((ch = fork())) {
case 0:
/* Child. Dispose of listeners and connections other than this
* one. */
vector_iterate(listeners, t) listener_delete((listener)t->v);
vector_delete(listeners);
listeners = NULL;
for (J = connections; J < connections + max_connections; ++J)
if (*J && *J != c) {
close((*J)->s);
(*J)->s = -1;
connection_delete(*J);
*J = NULL;
}
/* Do any post-fork cleanup defined by authenticators, and drop any
* cached data. */
authswitch_postfork();
authcache_close();
/* We never access mailspools as root. */
if (c->a->uid == 0) {
log_print(LOG_ERR, _("fork_child: client %s: authentication context has UID of 0"), c->idstr);
connection_sendresponse(c, 0, _("Everything's really bad"));
return 0;
}
/* Set our gid and uid to that appropriate for the mailspool, as
* decided by the auth switch. */
if (setgid(c->a->gid) == -1) {
log_print(LOG_ERR, "fork_child: setgid(%d): %m", c->a->gid);
connection_sendresponse(c, 0, _("Something bad happened, and I just can't go on. Sorry."));
return 0;
} else if (setuid(c->a->uid) == -1) {
log_print(LOG_ERR, "fork_child: setuid(%d): %m", c->a->uid);
connection_sendresponse(c, 0, _("Something bad happened, and I just can't go on. Sorry."));
return 0;
}
/* Waiting for ONLOGIN. */
if (childwait) {
char buf[1];
ssize_t x;
close(pp[1]);
while ((x = read(pp[0], buf, 1) == -1) && errno == EINTR);
if (x == -1) {
log_print(LOG_ERR, "fork_child: read: %m");
connection_sendresponse(c, 0, _("Something bad happened, and I just can't go on. Sorry."));
return 0;
}
close(pp[0]);
}
/* Get in to the `transaction' state, opening the mailbox. */
this_child_connection = c;
if (connection_start_transaction(c)) {
char s[512], *p;
strcpy(s, _("Welcome aboard!"));
strcat(s, " ");
p = s + strlen(s);
switch (c->m->num) {
case 0:
strcpy(p, _("You have no messages at all."));
break;
case 1:
strcat(p, _("You have exactly one message."));
break;
default:
sprintf(p, _("You have %d messages."), c->m->num);
break;
}
connection_sendresponse(c, 1, s);
} else {
connection_sendresponse(c, 0, _("Unable to open mailbox; it may be locked by another concurrent session."));
return 0;
}
break;
case -1:
/* Error. Note that this is, therefore, still the parent process,
* and we must set post_fork appropriately.... */
post_fork = 0;
sigprocmask(SIG_UNBLOCK, &chmask, NULL);
log_print(LOG_ERR, "fork_child: fork: %m");
connection_sendresponse(c, 0, _("Everything was fine until now, but suddenly I realise I just can't go on. Sorry."));
return 0;
default:
/* Parent. Dispose of our copy of this connection. */
post_fork = 0; /* Now SIGHUP will work again. */
/* Began session. We log a message in a known format, and call
* into the authentication drivers in case they want to do
* something with the information for POP-before-SMTP relaying. */
log_print(LOG_INFO, _("fork_child: %s: began session for `%s' with %s; child PID is %d"), c->idstr, c->a->user, c->a->auth, (int)ch);
authswitch_onlogin(c->a, c->remote_ip, c->local_ip);
if (childwait) {
close(pp[0]);
if (xwrite(pp[1], "\0", 1) == -1)
/* Not much we can do here. Hopefully the child will get
* an error from read. If not it will hang, which is
* very bad news. But that shouldn't happen. */
log_print(LOG_ERR, "fork_child: write: %m");
close(pp[1]);
}
/* Dispose of our copy of this connection. */
close(c->s);
c->s = -1;
remove_connection(c);
connection_delete(c);
c = NULL;
++num_running_children;
break;
}
/* Unblock SIGCHLD after incrementing num_running_children. */
sigprocmask(SIG_UNBLOCK, &chmask, NULL);
/* Success. */
return 1;
#undef c
#undef I
}
/* connections_post_select:
* Called after the main select(2) to do stuff with connections.
*
* For each connection, we call its own post_select routine. This will do all sorts
* of stuff which is hidden to us, including pushing the running/closing/closed
* state machine around and reading and writing the I/O buffers. We need to try to
* parse commands when it's indicated that data have been read, and react to the
* changed state of any connection. */
static void connections_post_select(fd_set *readfds, fd_set *writefds, fd_set *exceptfds) {
static size_t i;
size_t i0;
time_t start;
time(&start);
for (i0 = (i + max_connections - 1) % max_connections; time(NULL) < start + LATENCY && i != i0; i = (i + 1) % max_connections) {
connection c;
int r;
if (!(c = connections[i]))
continue;
/* Handle all post-select I/O. */
r = c->io->post_select(c, readfds, writefds, exceptfds);
/* At this stage, the connection may be closed or closing. But we
* should try to interpret commands anyway, in case the client sends
* QUIT and immediately closes the connection. */
if (r && !connection_isfrozen(c)) {
/*
* Handling of POP3 commands, and forking children to handle
* authenticated connections.
*/
pop3command p;
/* Process as many commands as we can.... */
while (c->cstate == running && (p = connection_parsecommand(c))) {
enum connection_action act;
act = connection_do(c, p);
pop3command_delete(p);
switch (act) {
case close_connection:
c->do_shutdown = 1;
break;
case fork_and_setuid:
if (num_running_children >= max_running_children) {
connection_sendresponse(c, 0, _("Sorry, I'm too busy right now"));
log_print(LOG_WARNING, _("connections_post_select: client %s: rejected login owing to high load"), c->idstr);
c->do_shutdown = 1;
} else {
if (!fork_child(c))
c->do_shutdown = 1;
/* If this is the parent process, c has now been destroyed. */
else if (!post_fork)
c = NULL;
}
break;
default:;
}
if (!c || c->do_shutdown)
break;
}
if (!c)
continue; /* if connection has been destroyed, do next one */
}
/* Timeout handling. */
if (timeout_seconds && (time(NULL) > (c->idlesince + timeout_seconds))) {
/* Connection has timed out. */
#ifndef NO_SNIDE_COMMENTS
connection_sendresponse(c, 0, _("You can hang around all day if you like. I have better things to do."));
#else
connection_sendresponse(c, 0, _("Client has been idle for too long."));
#endif
log_print(LOG_INFO, _("net_loop: timed out client %s"), c->idstr);
if (c->do_shutdown)
c->io->shutdown(c); /* immediate shutdown */
else
connection_shutdown(c); /* give a chance to flush buffer (in particular, the error message) */
}
/* Shut down the connection if requested, or if shutdown was
* requested when the connection was frozen and it is now thawed
* again, or when data remained to be written. */
if (c->do_shutdown)
connection_shutdown(c);
/*
* At this point, we need to find out whether this connection has been
* closed (i.e., transport completely shut down). If so, we need to
* destroy the connection, and, if this is a child process, exit, since
* we have no more work to do.
*/
if (c->cstate == closed) {
/* We should now log the closure of the connection and ending
* of any authenticated session. */
if (c->a)
log_print(LOG_INFO, _("connections_post_select: client %s: finished session for `%s' with %s"), c->idstr, c->a->user, c->a->auth);
log_print(LOG_INFO, _("connections_post_select: client %s: disconnected; %d/%d bytes read/written"), c->idstr, c->nrd, c->nwr);
/* remove_connection(c);*/
connections[i] = NULL;
connection_delete(c);
/* If this is a child process, we exit now. */
if (post_fork)
_exit(0);
}
}
}
/* net_loop
* Accept connections and put them into an appropriate state, calling
* setuid() and fork() when appropriate. */
sig_atomic_t foad = 0, restart = 0; /* Flags used to indicate that we should exit or should re-exec. */
void net_loop(void) {
connection *J;
#ifdef AUTH_OTHER
extern pid_t auth_other_childdied;
extern int auth_other_childstatus;
#endif /* AUTH_OTHER */
extern pid_t child_died;
extern int child_died_signal;
sigset_t chmask;
sigemptyset(&chmask);
sigaddset(&chmask, SIGCHLD);
/* 2 * max_running_children is a reasonable ball-park figure. */
max_connections = 2 * max_running_children;
connections = (connection*)xcalloc(max_connections, sizeof(connection*));
log_print(LOG_INFO, _("net_loop: tpop3d version %s successfully started"), TPOP3D_VERSION);
/* Main select() loop */
while (!foad) {
fd_set readfds, writefds;
struct timeval tv = {1, 0}; /* Must be less than IDLE_TIMEOUT and small enough that termination on receipt of SIGTERM is timely. */
int n = 0, e;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
if (!post_fork) listeners_pre_select(&n, &readfds, &writefds, NULL);
connections_pre_select(&n, &readfds, &writefds, NULL);
e = select(n + 1, &readfds, &writefds, NULL, &tv);
if (e == -1 && errno != EINTR) {
log_print(LOG_WARNING, "net_loop: select: %m");
} else if (e >= 0) {
/* Check for new incoming connections */
if (!post_fork) listeners_post_select(&readfds, &writefds, NULL);
/* Monitor existing connections */
connections_post_select(&readfds, &writefds, NULL);
}
sigprocmask(SIG_BLOCK, &chmask, NULL);
#ifdef AUTH_OTHER
/* It may be that the authentication child died; log the message here
* to avoid doing something we shouldn't in the signal handler. */
if (auth_other_childdied) {
log_print(LOG_WARNING, _("net_loop: authentication child %d terminated with status %d"), (int)auth_other_childdied, auth_other_childstatus);
auth_other_childdied = 0;
}
#endif /* AUTH_OTHER */
/* Also log a message if a child process died with a signal. */
if (child_died) {
log_print(LOG_ERR, _("net_loop: child process %d killed by signal %d (shouldn't happen)"), (int)child_died, child_died_signal);
child_died = 0;
}
sigprocmask(SIG_UNBLOCK, &chmask, NULL);
}
/* Termination request received; we should close all connections in an
* orderly fashion. */
if (restart)
log_print(LOG_INFO, _("net_loop: restarting on signal %d"), foad);
else
log_print(LOG_INFO, _("net_loop: terminating on signal %d"), foad);
if (connections) {
for (J = connections; J < connections + max_connections; ++J)
if (*J) connection_delete(*J);
xfree(connections);
}
}
syntax highlighted by Code2HTML, v. 0.9.1