/* * 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 #include #include #include #include #include #include #include #include #ifdef USE_TCP_WRAPPERS # include #endif #include #include #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); } }