/* thttpd.c - tiny/turbo/throttling HTTP server ** ** Copyright © 1995,1998,1999,2000,2001,2015 by ** Jef Poskanzer . 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. ** ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. */ #include "config.h" #include "version.h" #include #include #include #include #include #include #include #ifdef HAVE_FCNTL_H #include #endif #include #ifdef HAVE_GRP_H #include #endif #include #include #include #include #include #ifdef TIME_WITH_SYS_TIME #include #endif #include #include "fdwatch.h" #include "libhttpd.h" #include "mmc.h" #include "timers.h" #include "match.h" #ifndef SHUT_WR #define SHUT_WR 1 #endif #ifndef HAVE_INT64T typedef long long int64_t; #endif static char* argv0; static int debug; static unsigned short port; static char* dir; static char* data_dir; static int do_chroot, no_log, no_symlink_check, do_vhost, do_global_passwd; static char* cgi_pattern; static int cgi_limit; static char* url_pattern; static int no_empty_referrers; static char* local_pattern; static char* logfile; static char* throttlefile; static char* hostname; static char* pidfile; static char* user; static char* charset; static char* p3p; static int max_age; typedef struct { char* pattern; long max_limit, min_limit; long rate; off_t bytes_since_avg; int num_sending; } throttletab; static throttletab* throttles; static int numthrottles, maxthrottles; #define THROTTLE_NOLIMIT -1 typedef struct { int conn_state; int next_free_connect; httpd_conn* hc; int tnums[MAXTHROTTLENUMS]; /* throttle indexes */ int numtnums; long max_limit, min_limit; time_t started_at, active_at; Timer* wakeup_timer; Timer* linger_timer; long wouldblock_delay; off_t bytes; off_t end_byte_index; off_t next_byte_index; } connecttab; static connecttab* connects; static int num_connects, max_connects, first_free_connect; static int httpd_conn_count; /* The connection states. */ #define CNST_FREE 0 #define CNST_READING 1 #define CNST_SENDING 2 #define CNST_PAUSING 3 #define CNST_LINGERING 4 static httpd_server* hs = (httpd_server*) 0; int terminate = 0; time_t start_time, stats_time; long stats_connections; off_t stats_bytes; int stats_simultaneous; static volatile int got_hup, got_usr1, watchdog_flag; /* Forwards. */ static void parse_args( int argc, char** argv ); static void usage( void ); static void read_config( char* filename ); static void value_required( char* name, char* value ); static void no_value_required( char* name, char* value ); static char* e_strdup( char* oldstr ); static void lookup_hostname( httpd_sockaddr* sa4P, size_t sa4_len, int* gotv4P, httpd_sockaddr* sa6P, size_t sa6_len, int* gotv6P ); static void read_throttlefile( char* tf ); static void shut_down( void ); static int handle_newconnect( struct timeval* tvP, int listen_fd ); static void handle_read( connecttab* c, struct timeval* tvP ); static void handle_send( connecttab* c, struct timeval* tvP ); static void handle_linger( connecttab* c, struct timeval* tvP ); static int check_throttles( connecttab* c ); static void clear_throttles( connecttab* c, struct timeval* tvP ); static void update_throttles( ClientData client_data, struct timeval* nowP ); static void finish_connection( connecttab* c, struct timeval* tvP ); static void clear_connection( connecttab* c, struct timeval* tvP ); static void really_clear_connection( connecttab* c, struct timeval* tvP ); static void idle( ClientData client_data, struct timeval* nowP ); static void wakeup_connection( ClientData client_data, struct timeval* nowP ); static void linger_clear_connection( ClientData client_data, struct timeval* nowP ); static void occasional( ClientData client_data, struct timeval* nowP ); #ifdef STATS_TIME static void show_stats( ClientData client_data, struct timeval* nowP ); #endif /* STATS_TIME */ static void logstats( struct timeval* nowP ); static void thttpd_logstats( long secs ); /* SIGTERM and SIGINT say to exit immediately. */ static void handle_term( int sig ) { /* Don't need to set up the handler again, since it's a one-shot. */ shut_down(); syslog( LOG_NOTICE, "exiting due to signal %d", sig ); closelog(); exit( 1 ); } /* SIGCHLD - a chile process exitted, so we need to reap the zombie */ static void handle_chld( int sig ) { const int oerrno = errno; pid_t pid; int status; #ifndef HAVE_SIGSET /* Set up handler again. */ (void) signal( SIGCHLD, handle_chld ); #endif /* ! HAVE_SIGSET */ /* Reap defunct children until there aren't any more. */ for (;;) { #ifdef HAVE_WAITPID pid = waitpid( (pid_t) -1, &status, WNOHANG ); #else /* HAVE_WAITPID */ pid = wait3( &status, WNOHANG, (struct rusage*) 0 ); #endif /* HAVE_WAITPID */ if ( (int) pid == 0 ) /* none left */ break; if ( (int) pid < 0 ) { if ( errno == EINTR || errno == EAGAIN ) continue; /* ECHILD shouldn't happen with the WNOHANG option, ** but with some kernels it does anyway. Ignore it. */ if ( errno != ECHILD ) syslog( LOG_ERR, "child wait - %m" ); break; } /* Decrement the CGI count. Note that this is not accurate, since ** each CGI can involve two or even three child processes. ** Decrementing for each child means that when there is heavy CGI ** activity, the count will be lower than it should be, and therefore ** more CGIs will be allowed than should be. */ if ( hs != (httpd_server*) 0 ) { --hs->cgi_count; if ( hs->cgi_count < 0 ) hs->cgi_count = 0; } } /* Restore previous errno. */ errno = oerrno; } /* SIGHUP says to re-open the log file. */ static void handle_hup( int sig ) { const int oerrno = errno; #ifndef HAVE_SIGSET /* Set up handler again. */ (void) signal( SIGHUP, handle_hup ); #endif /* ! HAVE_SIGSET */ /* Just set a flag that we got the signal. */ got_hup = 1; /* Restore previous errno. */ errno = oerrno; } /* SIGUSR1 says to exit as soon as all current connections are done. */ static void handle_usr1( int sig ) { /* Don't need to set up the handler again, since it's a one-shot. */ if ( num_connects == 0 ) { /* If there are no active connections we want to exit immediately ** here. Not only is it faster, but without any connections the ** main loop won't wake up until the next new connection. */ shut_down(); syslog( LOG_NOTICE, "exiting" ); closelog(); exit( 0 ); } /* Otherwise, just set a flag that we got the signal. */ got_usr1 = 1; /* Don't need to restore old errno, since we didn't do any syscalls. */ } /* SIGUSR2 says to generate the stats syslogs immediately. */ static void handle_usr2( int sig ) { const int oerrno = errno; #ifndef HAVE_SIGSET /* Set up handler again. */ (void) signal( SIGUSR2, handle_usr2 ); #endif /* ! HAVE_SIGSET */ logstats( (struct timeval*) 0 ); /* Restore previous errno. */ errno = oerrno; } /* SIGALRM is used as a watchdog. */ static void handle_alrm( int sig ) { const int oerrno = errno; /* If nothing has been happening */ if ( ! watchdog_flag ) { /* Try changing dirs to someplace we can write. */ (void) chdir( "/tmp" ); /* Dump core. */ abort(); } watchdog_flag = 0; #ifndef HAVE_SIGSET /* Set up handler again. */ (void) signal( SIGALRM, handle_alrm ); #endif /* ! HAVE_SIGSET */ /* Set up alarm again. */ (void) alarm( OCCASIONAL_TIME * 3 ); /* Restore previous errno. */ errno = oerrno; } static void re_open_logfile( void ) { FILE* logfp; if ( no_log || hs == (httpd_server*) 0 ) return; /* Re-open the log file. */ if ( logfile != (char*) 0 && strcmp( logfile, "-" ) != 0 ) { syslog( LOG_NOTICE, "re-opening logfile" ); logfp = fopen( logfile, "a" ); if ( logfp == (FILE*) 0 ) { syslog( LOG_CRIT, "re-opening %.80s - %m", logfile ); return; } (void) fcntl( fileno( logfp ), F_SETFD, 1 ); httpd_set_logfp( hs, logfp ); } } int main( int argc, char** argv ) { char* cp; struct passwd* pwd; uid_t uid = 32767; gid_t gid = 32767; char cwd[MAXPATHLEN+1]; FILE* logfp; int num_ready; int cnum; connecttab* c; httpd_conn* hc; httpd_sockaddr sa4; httpd_sockaddr sa6; int gotv4, gotv6; struct timeval tv; argv0 = argv[0]; cp = strrchr( argv0, '/' ); if ( cp != (char*) 0 ) ++cp; else cp = argv0; openlog( cp, LOG_NDELAY|LOG_PID, LOG_FACILITY ); /* Handle command-line arguments. */ parse_args( argc, argv ); /* Read zone info now, in case we chroot(). */ tzset(); /* Look up hostname now, in case we chroot(). */ lookup_hostname( &sa4, sizeof(sa4), &gotv4, &sa6, sizeof(sa6), &gotv6 ); if ( ! ( gotv4 || gotv6 ) ) { syslog( LOG_ERR, "can't find any valid address" ); (void) fprintf( stderr, "%s: can't find any valid address\n", argv0 ); exit( 1 ); } /* Throttle file. */ numthrottles = 0; maxthrottles = 0; throttles = (throttletab*) 0; if ( throttlefile != (char*) 0 ) read_throttlefile( throttlefile ); /* If we're root and we're going to become another user, get the uid/gid ** now. */ if ( getuid() == 0 ) { pwd = getpwnam( user ); if ( pwd == (struct passwd*) 0 ) { syslog( LOG_CRIT, "unknown user - '%.80s'", user ); (void) fprintf( stderr, "%s: unknown user - '%s'\n", argv0, user ); exit( 1 ); } uid = pwd->pw_uid; gid = pwd->pw_gid; } /* Log file. */ if ( logfile != (char*) 0 ) { if ( strcmp( logfile, "/dev/null" ) == 0 ) { no_log = 1; logfp = (FILE*) 0; } else if ( strcmp( logfile, "-" ) == 0 ) logfp = stdout; else { logfp = fopen( logfile, "a" ); if ( logfp == (FILE*) 0 ) { syslog( LOG_CRIT, "%.80s - %m", logfile ); perror( logfile ); exit( 1 ); } if ( logfile[0] != '/' ) { syslog( LOG_WARNING, "logfile is not an absolute path, you may not be able to re-open it" ); (void) fprintf( stderr, "%s: logfile is not an absolute path, you may not be able to re-open it\n", argv0 ); } (void) fcntl( fileno( logfp ), F_SETFD, 1 ); if ( getuid() == 0 ) { /* If we are root then we chown the log file to the user we'll ** be switching to. */ if ( fchown( fileno( logfp ), uid, gid ) < 0 ) { syslog( LOG_WARNING, "fchown logfile - %m" ); perror( "fchown logfile" ); } } } } else logfp = (FILE*) 0; /* Switch directories if requested. */ if ( dir != (char*) 0 ) { if ( chdir( dir ) < 0 ) { syslog( LOG_CRIT, "chdir - %m" ); perror( "chdir" ); exit( 1 ); } } #ifdef USE_USER_DIR else if ( getuid() == 0 ) { /* No explicit directory was specified, we're root, and the ** USE_USER_DIR option is set - switch to the specified user's ** home dir. */ if ( chdir( pwd->pw_dir ) < 0 ) { syslog( LOG_CRIT, "chdir - %m" ); perror( "chdir" ); exit( 1 ); } } #endif /* USE_USER_DIR */ /* Get current directory. */ (void) getcwd( cwd, sizeof(cwd) - 1 ); if ( cwd[strlen( cwd ) - 1] != '/' ) (void) strcat( cwd, "/" ); if ( ! debug ) { /* We're not going to use stdin stdout or stderr from here on, so close ** them to save file descriptors. */ (void) fclose( stdin ); if ( logfp != stdout ) (void) fclose( stdout ); (void) fclose( stderr ); /* Daemonize - make ourselves a subprocess. */ #ifdef HAVE_DAEMON if ( daemon( 1, 1 ) < 0 ) { syslog( LOG_CRIT, "daemon - %m" ); exit( 1 ); } #else /* HAVE_DAEMON */ switch ( fork() ) { case 0: break; case -1: syslog( LOG_CRIT, "fork - %m" ); exit( 1 ); default: exit( 0 ); } #ifdef HAVE_SETSID (void) setsid(); #endif /* HAVE_SETSID */ #endif /* HAVE_DAEMON */ } else { /* Even if we don't daemonize, we still want to disown our parent ** process. */ #ifdef HAVE_SETSID (void) setsid(); #endif /* HAVE_SETSID */ } if ( pidfile != (char*) 0 ) { /* Write the PID file. */ FILE* pidfp = fopen( pidfile, "w" ); if ( pidfp == (FILE*) 0 ) { syslog( LOG_CRIT, "%.80s - %m", pidfile ); exit( 1 ); } (void) fprintf( pidfp, "%d\n", (int) getpid() ); (void) fclose( pidfp ); } /* Initialize the fdwatch package. Have to do this before chroot, ** if /dev/poll is used. */ max_connects = fdwatch_get_nfiles(); if ( max_connects < 0 ) { syslog( LOG_CRIT, "fdwatch initialization failure" ); exit( 1 ); } max_connects -= SPARE_FDS; /* Chroot if requested. */ if ( do_chroot ) { if ( chroot( cwd ) < 0 ) { syslog( LOG_CRIT, "chroot - %m" ); perror( "chroot" ); exit( 1 ); } /* If we're logging and the logfile's pathname begins with the ** chroot tree's pathname, then elide the chroot pathname so ** that the logfile pathname still works from inside the chroot ** tree. */ if ( logfile != (char*) 0 && strcmp( logfile, "-" ) != 0 ) { if ( strncmp( logfile, cwd, strlen( cwd ) ) == 0 ) { (void) ol_strcpy( logfile, &logfile[strlen( cwd ) - 1] ); /* (We already guaranteed that cwd ends with a slash, so leaving ** that slash in logfile makes it an absolute pathname within ** the chroot tree.) */ } else { syslog( LOG_WARNING, "logfile is not within the chroot tree, you will not be able to re-open it" ); (void) fprintf( stderr, "%s: logfile is not within the chroot tree, you will not be able to re-open it\n", argv0 ); } } (void) strcpy( cwd, "/" ); /* Always chdir to / after a chroot. */ if ( chdir( cwd ) < 0 ) { syslog( LOG_CRIT, "chroot chdir - %m" ); perror( "chroot chdir" ); exit( 1 ); } } /* Switch directories again if requested. */ if ( data_dir != (char*) 0 ) { if ( chdir( data_dir ) < 0 ) { syslog( LOG_CRIT, "data_dir chdir - %m" ); perror( "data_dir chdir" ); exit( 1 ); } } /* Set up to catch signals. */ #ifdef HAVE_SIGSET (void) sigset( SIGTERM, handle_term ); (void) sigset( SIGINT, handle_term ); (void) sigset( SIGCHLD, handle_chld ); (void) sigset( SIGPIPE, SIG_IGN ); /* get EPIPE instead */ (void) sigset( SIGHUP, handle_hup ); (void) sigset( SIGUSR1, handle_usr1 ); (void) sigset( SIGUSR2, handle_usr2 ); (void) sigset( SIGALRM, handle_alrm ); #else /* HAVE_SIGSET */ (void) signal( SIGTERM, handle_term ); (void) signal( SIGINT, handle_term ); (void) signal( SIGCHLD, handle_chld ); (void) signal( SIGPIPE, SIG_IGN ); /* get EPIPE instead */ (void) signal( SIGHUP, handle_hup ); (void) signal( SIGUSR1, handle_usr1 ); (void) signal( SIGUSR2, handle_usr2 ); (void) signal( SIGALRM, handle_alrm ); #endif /* HAVE_SIGSET */ got_hup = 0; got_usr1 = 0; watchdog_flag = 0; (void) alarm( OCCASIONAL_TIME * 3 ); /* Initialize the timer package. */ tmr_init(); /* Initialize the HTTP layer. Got to do this before giving up root, ** so that we can bind to a privileged port. */ hs = httpd_initialize( hostname, gotv4 ? &sa4 : (httpd_sockaddr*) 0, gotv6 ? &sa6 : (httpd_sockaddr*) 0, port, cgi_pattern, cgi_limit, charset, p3p, max_age, cwd, no_log, logfp, no_symlink_check, do_vhost, do_global_passwd, url_pattern, local_pattern, no_empty_referrers ); if ( hs == (httpd_server*) 0 ) exit( 1 ); /* Set up the occasional timer. */ if ( tmr_create( (struct timeval*) 0, occasional, JunkClientData, OCCASIONAL_TIME * 1000L, 1 ) == (Timer*) 0 ) { syslog( LOG_CRIT, "tmr_create(occasional) failed" ); exit( 1 ); } /* Set up the idle timer. */ if ( tmr_create( (struct timeval*) 0, idle, JunkClientData, 5 * 1000L, 1 ) == (Timer*) 0 ) { syslog( LOG_CRIT, "tmr_create(idle) failed" ); exit( 1 ); } if ( numthrottles > 0 ) { /* Set up the throttles timer. */ if ( tmr_create( (struct timeval*) 0, update_throttles, JunkClientData, THROTTLE_TIME * 1000L, 1 ) == (Timer*) 0 ) { syslog( LOG_CRIT, "tmr_create(update_throttles) failed" ); exit( 1 ); } } #ifdef STATS_TIME /* Set up the stats timer. */ if ( tmr_create( (struct timeval*) 0, show_stats, JunkClientData, STATS_TIME * 1000L, 1 ) == (Timer*) 0 ) { syslog( LOG_CRIT, "tmr_create(show_stats) failed" ); exit( 1 ); } #endif /* STATS_TIME */ start_time = stats_time = time( (time_t*) 0 ); stats_connections = 0; stats_bytes = 0; stats_simultaneous = 0; /* If we're root, try to become someone else. */ if ( getuid() == 0 ) { /* Set aux groups to null. */ if ( setgroups( 0, (const gid_t*) 0 ) < 0 ) { syslog( LOG_CRIT, "setgroups - %m" ); exit( 1 ); } /* Set primary group. */ if ( setgid( gid ) < 0 ) { syslog( LOG_CRIT, "setgid - %m" ); exit( 1 ); } /* Try setting aux groups correctly - not critical if this fails. */ if ( initgroups( user, gid ) < 0 ) syslog( LOG_WARNING, "initgroups - %m" ); #ifdef HAVE_SETLOGIN /* Set login name. */ (void) setlogin( user ); #endif /* HAVE_SETLOGIN */ /* Set uid. */ if ( setuid( uid ) < 0 ) { syslog( LOG_CRIT, "setuid - %m" ); exit( 1 ); } /* Check for unnecessary security exposure. */ if ( ! do_chroot ) syslog( LOG_WARNING, "started as root without requesting chroot(), warning only" ); } /* Initialize our connections table. */ connects = NEW( connecttab, max_connects ); if ( connects == (connecttab*) 0 ) { syslog( LOG_CRIT, "out of memory allocating a connecttab" ); exit( 1 ); } for ( cnum = 0; cnum < max_connects; ++cnum ) { connects[cnum].conn_state = CNST_FREE; connects[cnum].next_free_connect = cnum + 1; connects[cnum].hc = (httpd_conn*) 0; } connects[max_connects - 1].next_free_connect = -1; /* end of link list */ first_free_connect = 0; num_connects = 0; httpd_conn_count = 0; if ( hs != (httpd_server*) 0 ) { if ( hs->listen4_fd != -1 ) fdwatch_add_fd( hs->listen4_fd, (void*) 0, FDW_READ ); if ( hs->listen6_fd != -1 ) fdwatch_add_fd( hs->listen6_fd, (void*) 0, FDW_READ ); } /* Main loop. */ (void) gettimeofday( &tv, (struct timezone*) 0 ); while ( ( ! terminate ) || num_connects > 0 ) { /* Do we need to re-open the log file? */ if ( got_hup ) { re_open_logfile(); got_hup = 0; } /* Do the fd watch. */ num_ready = fdwatch( tmr_mstimeout( &tv ) ); if ( num_ready < 0 ) { if ( errno == EINTR || errno == EAGAIN ) continue; /* try again */ syslog( LOG_ERR, "fdwatch - %m" ); exit( 1 ); } (void) gettimeofday( &tv, (struct timezone*) 0 ); if ( num_ready == 0 ) { /* No fd's are ready - run the timers. */ tmr_run( &tv ); continue; } /* Is it a new connection? */ if ( hs != (httpd_server*) 0 && hs->listen6_fd != -1 && fdwatch_check_fd( hs->listen6_fd ) ) { if ( handle_newconnect( &tv, hs->listen6_fd ) ) /* Go around the loop and do another fdwatch, rather than ** dropping through and processing existing connections. ** New connections always get priority. */ continue; } if ( hs != (httpd_server*) 0 && hs->listen4_fd != -1 && fdwatch_check_fd( hs->listen4_fd ) ) { if ( handle_newconnect( &tv, hs->listen4_fd ) ) /* Go around the loop and do another fdwatch, rather than ** dropping through and processing existing connections. ** New connections always get priority. */ continue; } /* Find the connections that need servicing. */ while ( ( c = (connecttab*) fdwatch_get_next_client_data() ) != (connecttab*) -1 ) { if ( c == (connecttab*) 0 ) continue; hc = c->hc; if ( ! fdwatch_check_fd( hc->conn_fd ) ) /* Something went wrong. */ clear_connection( c, &tv ); else switch ( c->conn_state ) { case CNST_READING: handle_read( c, &tv ); break; case CNST_SENDING: handle_send( c, &tv ); break; case CNST_LINGERING: handle_linger( c, &tv ); break; } } tmr_run( &tv ); if ( got_usr1 && ! terminate ) { terminate = 1; if ( hs != (httpd_server*) 0 ) { if ( hs->listen4_fd != -1 ) fdwatch_del_fd( hs->listen4_fd ); if ( hs->listen6_fd != -1 ) fdwatch_del_fd( hs->listen6_fd ); httpd_unlisten( hs ); } } } /* The main loop terminated. */ shut_down(); syslog( LOG_NOTICE, "exiting" ); closelog(); exit( 0 ); } static void parse_args( int argc, char** argv ) { int argn; debug = 0; port = DEFAULT_PORT; dir = (char*) 0; data_dir = (char*) 0; #ifdef ALWAYS_CHROOT do_chroot = 1; #else /* ALWAYS_CHROOT */ do_chroot = 0; #endif /* ALWAYS_CHROOT */ no_log = 0; no_symlink_check = do_chroot; #ifdef ALWAYS_VHOST do_vhost = 1; #else /* ALWAYS_VHOST */ do_vhost = 0; #endif /* ALWAYS_VHOST */ #ifdef ALWAYS_GLOBAL_PASSWD do_global_passwd = 1; #else /* ALWAYS_GLOBAL_PASSWD */ do_global_passwd = 0; #endif /* ALWAYS_GLOBAL_PASSWD */ #ifdef CGI_PATTERN cgi_pattern = CGI_PATTERN; #else /* CGI_PATTERN */ cgi_pattern = (char*) 0; #endif /* CGI_PATTERN */ #ifdef CGI_LIMIT cgi_limit = CGI_LIMIT; #else /* CGI_LIMIT */ cgi_limit = 0; #endif /* CGI_LIMIT */ url_pattern = (char*) 0; no_empty_referrers = 0; local_pattern = (char*) 0; throttlefile = (char*) 0; hostname = (char*) 0; logfile = (char*) 0; pidfile = (char*) 0; user = DEFAULT_USER; charset = DEFAULT_CHARSET; p3p = ""; max_age = -1; argn = 1; while ( argn < argc && argv[argn][0] == '-' ) { if ( strcmp( argv[argn], "-V" ) == 0 ) { (void) printf( "%s\n", SERVER_SOFTWARE ); exit( 0 ); } else if ( strcmp( argv[argn], "-C" ) == 0 && argn + 1 < argc ) { ++argn; read_config( argv[argn] ); } else if ( strcmp( argv[argn], "-p" ) == 0 && argn + 1 < argc ) { ++argn; port = (unsigned short) atoi( argv[argn] ); } else if ( strcmp( argv[argn], "-d" ) == 0 && argn + 1 < argc ) { ++argn; dir = argv[argn]; } else if ( strcmp( argv[argn], "-r" ) == 0 ) { do_chroot = 1; no_symlink_check = 1; } else if ( strcmp( argv[argn], "-nor" ) == 0 ) { do_chroot = 0; no_symlink_check = 0; } else if ( strcmp( argv[argn], "-dd" ) == 0 && argn + 1 < argc ) { ++argn; data_dir = argv[argn]; } else if ( strcmp( argv[argn], "-s" ) == 0 ) no_symlink_check = 0; else if ( strcmp( argv[argn], "-nos" ) == 0 ) no_symlink_check = 1; else if ( strcmp( argv[argn], "-u" ) == 0 && argn + 1 < argc ) { ++argn; user = argv[argn]; } else if ( strcmp( argv[argn], "-c" ) == 0 && argn + 1 < argc ) { ++argn; cgi_pattern = argv[argn]; } else if ( strcmp( argv[argn], "-t" ) == 0 && argn + 1 < argc ) { ++argn; throttlefile = argv[argn]; } else if ( strcmp( argv[argn], "-h" ) == 0 && argn + 1 < argc ) { ++argn; hostname = argv[argn]; } else if ( strcmp( argv[argn], "-l" ) == 0 && argn + 1 < argc ) { ++argn; logfile = argv[argn]; } else if ( strcmp( argv[argn], "-v" ) == 0 ) do_vhost = 1; else if ( strcmp( argv[argn], "-nov" ) == 0 ) do_vhost = 0; else if ( strcmp( argv[argn], "-g" ) == 0 ) do_global_passwd = 1; else if ( strcmp( argv[argn], "-nog" ) == 0 ) do_global_passwd = 0; else if ( strcmp( argv[argn], "-i" ) == 0 && argn + 1 < argc ) { ++argn; pidfile = argv[argn]; } else if ( strcmp( argv[argn], "-T" ) == 0 && argn + 1 < argc ) { ++argn; charset = argv[argn]; } else if ( strcmp( argv[argn], "-P" ) == 0 && argn + 1 < argc ) { ++argn; p3p = argv[argn]; } else if ( strcmp( argv[argn], "-M" ) == 0 && argn + 1 < argc ) { ++argn; max_age = atoi( argv[argn] ); } else if ( strcmp( argv[argn], "-D" ) == 0 ) debug = 1; else usage(); ++argn; } if ( argn != argc ) usage(); } static void usage( void ) { (void) fprintf( stderr, "usage: %s [-C configfile] [-p port] [-d dir] [-r|-nor] [-dd data_dir] [-s|-nos] [-v|-nov] [-g|-nog] [-u user] [-c cgipat] [-t throttles] [-h host] [-l logfile] [-i pidfile] [-T charset] [-P P3P] [-M maxage] [-V] [-D]\n", argv0 ); exit( 1 ); } static void read_config( char* filename ) { FILE* fp; char line[10000]; char* cp; char* cp2; char* name; char* value; fp = fopen( filename, "r" ); if ( fp == (FILE*) 0 ) { perror( filename ); exit( 1 ); } while ( fgets( line, sizeof(line), fp ) != (char*) 0 ) { /* Trim comments. */ if ( ( cp = strchr( line, '#' ) ) != (char*) 0 ) *cp = '\0'; /* Skip leading whitespace. */ cp = line; cp += strspn( cp, " \t\n\r" ); /* Split line into words. */ while ( *cp != '\0' ) { /* Find next whitespace. */ cp2 = cp + strcspn( cp, " \t\n\r" ); /* Insert EOS and advance next-word pointer. */ while ( *cp2 == ' ' || *cp2 == '\t' || *cp2 == '\n' || *cp2 == '\r' ) *cp2++ = '\0'; /* Split into name and value. */ name = cp; value = strchr( name, '=' ); if ( value != (char*) 0 ) *value++ = '\0'; /* Interpret. */ if ( strcasecmp( name, "debug" ) == 0 ) { no_value_required( name, value ); debug = 1; } else if ( strcasecmp( name, "port" ) == 0 ) { value_required( name, value ); port = (unsigned short) atoi( value ); } else if ( strcasecmp( name, "dir" ) == 0 ) { value_required( name, value ); dir = e_strdup( value ); } else if ( strcasecmp( name, "chroot" ) == 0 ) { no_value_required( name, value ); do_chroot = 1; no_symlink_check = 1; } else if ( strcasecmp( name, "nochroot" ) == 0 ) { no_value_required( name, value ); do_chroot = 0; no_symlink_check = 0; } else if ( strcasecmp( name, "data_dir" ) == 0 ) { value_required( name, value ); data_dir = e_strdup( value ); } else if ( strcasecmp( name, "nosymlinkcheck" ) == 0 ) { no_value_required( name, value ); no_symlink_check = 1; } else if ( strcasecmp( name, "symlinkcheck" ) == 0 ) { no_value_required( name, value ); no_symlink_check = 0; } else if ( strcasecmp( name, "user" ) == 0 ) { value_required( name, value ); user = e_strdup( value ); } else if ( strcasecmp( name, "cgipat" ) == 0 ) { value_required( name, value ); cgi_pattern = e_strdup( value ); } else if ( strcasecmp( name, "cgilimit" ) == 0 ) { value_required( name, value ); cgi_limit = atoi( value ); } else if ( strcasecmp( name, "urlpat" ) == 0 ) { value_required( name, value ); url_pattern = e_strdup( value ); } else if ( strcasecmp( name, "noemptyreferers" ) == 0 || strcasecmp( name, "noemptyreferrers" ) == 0 ) { no_value_required( name, value ); no_empty_referrers = 1; } else if ( strcasecmp( name, "localpat" ) == 0 ) { value_required( name, value ); local_pattern = e_strdup( value ); } else if ( strcasecmp( name, "throttles" ) == 0 ) { value_required( name, value ); throttlefile = e_strdup( value ); } else if ( strcasecmp( name, "host" ) == 0 ) { value_required( name, value ); hostname = e_strdup( value ); } else if ( strcasecmp( name, "logfile" ) == 0 ) { value_required( name, value ); logfile = e_strdup( value ); } else if ( strcasecmp( name, "vhost" ) == 0 ) { no_value_required( name, value ); do_vhost = 1; } else if ( strcasecmp( name, "novhost" ) == 0 ) { no_value_required( name, value ); do_vhost = 0; } else if ( strcasecmp( name, "globalpasswd" ) == 0 ) { no_value_required( name, value ); do_global_passwd = 1; } else if ( strcasecmp( name, "noglobalpasswd" ) == 0 ) { no_value_required( name, value ); do_global_passwd = 0; } else if ( strcasecmp( name, "pidfile" ) == 0 ) { value_required( name, value ); pidfile = e_strdup( value ); } else if ( strcasecmp( name, "charset" ) == 0 ) { value_required( name, value ); charset = e_strdup( value ); } else if ( strcasecmp( name, "p3p" ) == 0 ) { value_required( name, value ); p3p = e_strdup( value ); } else if ( strcasecmp( name, "max_age" ) == 0 ) { value_required( name, value ); max_age = atoi( value ); } else { (void) fprintf( stderr, "%s: unknown config option '%s'\n", argv0, name ); exit( 1 ); } /* Advance to next word. */ cp = cp2; cp += strspn( cp, " \t\n\r" ); } } (void) fclose( fp ); } static void value_required( char* name, char* value ) { if ( value == (char*) 0 ) { (void) fprintf( stderr, "%s: value required for %s option\n", argv0, name ); exit( 1 ); } } static void no_value_required( char* name, char* value ) { if ( value != (char*) 0 ) { (void) fprintf( stderr, "%s: no value required for %s option\n", argv0, name ); exit( 1 ); } } static char* e_strdup( char* oldstr ) { char* newstr; newstr = strdup( oldstr ); if ( newstr == (char*) 0 ) { syslog( LOG_CRIT, "out of memory copying a string" ); (void) fprintf( stderr, "%s: out of memory copying a string\n", argv0 ); exit( 1 ); } return newstr; } static void lookup_hostname( httpd_sockaddr* sa4P, size_t sa4_len, int* gotv4P, httpd_sockaddr* sa6P, size_t sa6_len, int* gotv6P ) { #ifdef USE_IPV6 struct addrinfo hints; char portstr[10]; int gaierr; struct addrinfo* ai; struct addrinfo* ai2; struct addrinfo* aiv6; struct addrinfo* aiv4; (void) memset( &hints, 0, sizeof(hints) ); hints.ai_family = PF_UNSPEC; hints.ai_flags = AI_PASSIVE; hints.ai_socktype = SOCK_STREAM; (void) snprintf( portstr, sizeof(portstr), "%d", (int) port ); if ( (gaierr = getaddrinfo( hostname, portstr, &hints, &ai )) != 0 ) { syslog( LOG_CRIT, "getaddrinfo %.80s - %.80s", hostname, gai_strerror( gaierr ) ); (void) fprintf( stderr, "%s: getaddrinfo %s - %s\n", argv0, hostname, gai_strerror( gaierr ) ); exit( 1 ); } /* Find the first IPv6 and IPv4 entries. */ aiv6 = (struct addrinfo*) 0; aiv4 = (struct addrinfo*) 0; for ( ai2 = ai; ai2 != (struct addrinfo*) 0; ai2 = ai2->ai_next ) { switch ( ai2->ai_family ) { case AF_INET6: if ( aiv6 == (struct addrinfo*) 0 ) aiv6 = ai2; break; case AF_INET: if ( aiv4 == (struct addrinfo*) 0 ) aiv4 = ai2; break; } } if ( aiv6 == (struct addrinfo*) 0 ) *gotv6P = 0; else { if ( sa6_len < aiv6->ai_addrlen ) { syslog( LOG_CRIT, "%.80s - sockaddr too small (%lu < %lu)", hostname, (unsigned long) sa6_len, (unsigned long) aiv6->ai_addrlen ); exit( 1 ); } (void) memset( sa6P, 0, sa6_len ); (void) memmove( sa6P, aiv6->ai_addr, aiv6->ai_addrlen ); *gotv6P = 1; } if ( aiv4 == (struct addrinfo*) 0 ) *gotv4P = 0; else { if ( sa4_len < aiv4->ai_addrlen ) { syslog( LOG_CRIT, "%.80s - sockaddr too small (%lu < %lu)", hostname, (unsigned long) sa4_len, (unsigned long) aiv4->ai_addrlen ); exit( 1 ); } (void) memset( sa4P, 0, sa4_len ); (void) memmove( sa4P, aiv4->ai_addr, aiv4->ai_addrlen ); *gotv4P = 1; } freeaddrinfo( ai ); #else /* USE_IPV6 */ struct hostent* he; *gotv6P = 0; (void) memset( sa4P, 0, sa4_len ); sa4P->sa.sa_family = AF_INET; if ( hostname == (char*) 0 ) sa4P->sa_in.sin_addr.s_addr = htonl( INADDR_ANY ); else { sa4P->sa_in.sin_addr.s_addr = inet_addr( hostname ); if ( (int) sa4P->sa_in.sin_addr.s_addr == -1 ) { he = gethostbyname( hostname ); if ( he == (struct hostent*) 0 ) { #ifdef HAVE_HSTRERROR syslog( LOG_CRIT, "gethostbyname %.80s - %.80s", hostname, hstrerror( h_errno ) ); (void) fprintf( stderr, "%s: gethostbyname %s - %s\n", argv0, hostname, hstrerror( h_errno ) ); #else /* HAVE_HSTRERROR */ syslog( LOG_CRIT, "gethostbyname %.80s failed", hostname ); (void) fprintf( stderr, "%s: gethostbyname %s failed\n", argv0, hostname ); #endif /* HAVE_HSTRERROR */ exit( 1 ); } if ( he->h_addrtype != AF_INET ) { syslog( LOG_CRIT, "%.80s - non-IP network address", hostname ); (void) fprintf( stderr, "%s: %s - non-IP network address\n", argv0, hostname ); exit( 1 ); } (void) memmove( &sa4P->sa_in.sin_addr.s_addr, he->h_addr, he->h_length ); } } sa4P->sa_in.sin_port = htons( port ); *gotv4P = 1; #endif /* USE_IPV6 */ } static void read_throttlefile( char* tf ) { FILE* fp; char buf[5000]; char* cp; int len; char pattern[5000]; long max_limit, min_limit; struct timeval tv; fp = fopen( tf, "r" ); if ( fp == (FILE*) 0 ) { syslog( LOG_CRIT, "%.80s - %m", tf ); perror( tf ); exit( 1 ); } (void) gettimeofday( &tv, (struct timezone*) 0 ); while ( fgets( buf, sizeof(buf), fp ) != (char*) 0 ) { /* Nuke comments. */ cp = strchr( buf, '#' ); if ( cp != (char*) 0 ) *cp = '\0'; /* Nuke trailing whitespace. */ len = strlen( buf ); while ( len > 0 && ( buf[len-1] == ' ' || buf[len-1] == '\t' || buf[len-1] == '\n' || buf[len-1] == '\r' ) ) buf[--len] = '\0'; /* Ignore empty lines. */ if ( len == 0 ) continue; /* Parse line. */ if ( sscanf( buf, " %4900[^ \t] %ld-%ld", pattern, &min_limit, &max_limit ) == 3 ) {} else if ( sscanf( buf, " %4900[^ \t] %ld", pattern, &max_limit ) == 2 ) min_limit = 0; else { syslog( LOG_CRIT, "unparsable line in %.80s - %.80s", tf, buf ); (void) fprintf( stderr, "%s: unparsable line in %.80s - %.80s\n", argv0, tf, buf ); continue; } /* Nuke any leading slashes in pattern. */ if ( pattern[0] == '/' ) (void) ol_strcpy( pattern, &pattern[1] ); while ( ( cp = strstr( pattern, "|/" ) ) != (char*) 0 ) (void) ol_strcpy( cp + 1, cp + 2 ); /* Check for room in throttles. */ if ( numthrottles >= maxthrottles ) { if ( maxthrottles == 0 ) { maxthrottles = 100; /* arbitrary */ throttles = NEW( throttletab, maxthrottles ); } else { maxthrottles *= 2; throttles = RENEW( throttles, throttletab, maxthrottles ); } if ( throttles == (throttletab*) 0 ) { syslog( LOG_CRIT, "out of memory allocating a throttletab" ); (void) fprintf( stderr, "%s: out of memory allocating a throttletab\n", argv0 ); exit( 1 ); } } /* Add to table. */ throttles[numthrottles].pattern = e_strdup( pattern ); throttles[numthrottles].max_limit = max_limit; throttles[numthrottles].min_limit = min_limit; throttles[numthrottles].rate = 0; throttles[numthrottles].bytes_since_avg = 0; throttles[numthrottles].num_sending = 0; ++numthrottles; } (void) fclose( fp ); } static void shut_down( void ) { int cnum; struct timeval tv; (void) gettimeofday( &tv, (struct timezone*) 0 ); logstats( &tv ); for ( cnum = 0; cnum < max_connects; ++cnum ) { if ( connects[cnum].conn_state != CNST_FREE ) httpd_close_conn( connects[cnum].hc, &tv ); if ( connects[cnum].hc != (httpd_conn*) 0 ) { httpd_destroy_conn( connects[cnum].hc ); free( (void*) connects[cnum].hc ); --httpd_conn_count; connects[cnum].hc = (httpd_conn*) 0; } } if ( hs != (httpd_server*) 0 ) { httpd_server* ths = hs; hs = (httpd_server*) 0; if ( ths->listen4_fd != -1 ) fdwatch_del_fd( ths->listen4_fd ); if ( ths->listen6_fd != -1 ) fdwatch_del_fd( ths->listen6_fd ); httpd_terminate( ths ); } mmc_term(); tmr_term(); free( (void*) connects ); if ( throttles != (throttletab*) 0 ) free( (void*) throttles ); } static int handle_newconnect( struct timeval* tvP, int listen_fd ) { connecttab* c; ClientData client_data; /* This loops until the accept() fails, trying to start new ** connections as fast as possible so we don't overrun the ** listen queue. */ for (;;) { /* Is there room in the connection table? */ if ( num_connects >= max_connects ) { /* Out of connection slots. Run the timers, then the ** existing connections, and maybe we'll free up a slot ** by the time we get back here. */ syslog( LOG_WARNING, "too many connections!" ); tmr_run( tvP ); return 0; } /* Get the first free connection entry off the free list. */ if ( first_free_connect == -1 || connects[first_free_connect].conn_state != CNST_FREE ) { syslog( LOG_CRIT, "the connects free list is messed up" ); exit( 1 ); } c = &connects[first_free_connect]; /* Make the httpd_conn if necessary. */ if ( c->hc == (httpd_conn*) 0 ) { c->hc = NEW( httpd_conn, 1 ); if ( c->hc == (httpd_conn*) 0 ) { syslog( LOG_CRIT, "out of memory allocating an httpd_conn" ); exit( 1 ); } c->hc->initialized = 0; ++httpd_conn_count; } /* Get the connection. */ switch ( httpd_get_conn( hs, listen_fd, c->hc ) ) { /* Some error happened. Run the timers, then the ** existing connections. Maybe the error will clear. */ case GC_FAIL: tmr_run( tvP ); return 0; /* No more connections to accept for now. */ case GC_NO_MORE: return 1; } c->conn_state = CNST_READING; /* Pop it off the free list. */ first_free_connect = c->next_free_connect; c->next_free_connect = -1; ++num_connects; client_data.p = c; c->active_at = tvP->tv_sec; c->wakeup_timer = (Timer*) 0; c->linger_timer = (Timer*) 0; c->next_byte_index = 0; c->numtnums = 0; /* Set the connection file descriptor to no-delay mode. */ httpd_set_ndelay( c->hc->conn_fd ); fdwatch_add_fd( c->hc->conn_fd, c, FDW_READ ); ++stats_connections; if ( num_connects > stats_simultaneous ) stats_simultaneous = num_connects; } } static void handle_read( connecttab* c, struct timeval* tvP ) { int sz; ClientData client_data; httpd_conn* hc = c->hc; /* Is there room in our buffer to read more bytes? */ if ( hc->read_idx >= hc->read_size ) { if ( hc->read_size > 5000 ) { httpd_send_err( hc, 400, httpd_err400title, "", httpd_err400form, "" ); finish_connection( c, tvP ); return; } httpd_realloc_str( &hc->read_buf, &hc->read_size, hc->read_size + 1000 ); } /* Read some more bytes. */ sz = read( hc->conn_fd, &(hc->read_buf[hc->read_idx]), hc->read_size - hc->read_idx ); if ( sz == 0 ) { httpd_send_err( hc, 400, httpd_err400title, "", httpd_err400form, "" ); finish_connection( c, tvP ); return; } if ( sz < 0 ) { /* Ignore EINTR and EAGAIN. Also ignore EWOULDBLOCK. At first glance ** you would think that connections returned by fdwatch as readable ** should never give an EWOULDBLOCK; however, this apparently can ** happen if a packet gets garbled. */ if ( errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK ) return; httpd_send_err( hc, 400, httpd_err400title, "", httpd_err400form, "" ); finish_connection( c, tvP ); return; } hc->read_idx += sz; c->active_at = tvP->tv_sec; /* Do we have a complete request yet? */ switch ( httpd_got_request( hc ) ) { case GR_NO_REQUEST: return; case GR_BAD_REQUEST: httpd_send_err( hc, 400, httpd_err400title, "", httpd_err400form, "" ); finish_connection( c, tvP ); return; } /* Yes. Try parsing and resolving it. */ if ( httpd_parse_request( hc ) < 0 ) { finish_connection( c, tvP ); return; } /* Check the throttle table */ if ( ! check_throttles( c ) ) { httpd_send_err( hc, 503, httpd_err503title, "", httpd_err503form, hc->encodedurl ); finish_connection( c, tvP ); return; } /* Start the connection going. */ if ( httpd_start_request( hc, tvP ) < 0 ) { /* Something went wrong. Close down the connection. */ finish_connection( c, tvP ); return; } /* Fill in end_byte_index. */ if ( hc->got_range ) { c->next_byte_index = hc->first_byte_index; c->end_byte_index = hc->last_byte_index + 1; } else if ( hc->bytes_to_send < 0 ) c->end_byte_index = 0; else c->end_byte_index = hc->bytes_to_send; /* Check if it's already handled. */ if ( hc->file_address == (char*) 0 ) { /* No file address means someone else is handling it. */ int tind; for ( tind = 0; tind < c->numtnums; ++tind ) throttles[c->tnums[tind]].bytes_since_avg += hc->bytes_sent; c->next_byte_index = hc->bytes_sent; finish_connection( c, tvP ); return; } if ( c->next_byte_index >= c->end_byte_index ) { /* There's nothing to send. */ finish_connection( c, tvP ); return; } /* Cool, we have a valid connection and a file to send to it. */ c->conn_state = CNST_SENDING; c->started_at = tvP->tv_sec; c->wouldblock_delay = 0; client_data.p = c; fdwatch_del_fd( hc->conn_fd ); fdwatch_add_fd( hc->conn_fd, c, FDW_WRITE ); } static void handle_send( connecttab* c, struct timeval* tvP ) { size_t max_bytes; int sz, coast; ClientData client_data; time_t elapsed; httpd_conn* hc = c->hc; int tind; if ( c->max_limit == THROTTLE_NOLIMIT ) max_bytes = 1000000000L; else max_bytes = c->max_limit / 4; /* send at most 1/4 seconds worth */ /* Do we need to write the headers first? */ if ( hc->responselen == 0 ) { /* No, just write the file. */ sz = write( hc->conn_fd, &(hc->file_address[c->next_byte_index]), MIN( c->end_byte_index - c->next_byte_index, max_bytes ) ); } else { /* Yes. We'll combine headers and file into a single writev(), ** hoping that this generates a single packet. */ struct iovec iv[2]; iv[0].iov_base = hc->response; iv[0].iov_len = hc->responselen; iv[1].iov_base = &(hc->file_address[c->next_byte_index]); iv[1].iov_len = MIN( c->end_byte_index - c->next_byte_index, max_bytes ); sz = writev( hc->conn_fd, iv, 2 ); } if ( sz < 0 && errno == EINTR ) return; if ( sz == 0 || ( sz < 0 && ( errno == EWOULDBLOCK || errno == EAGAIN ) ) ) { /* This shouldn't happen, but some kernels, e.g. ** SunOS 4.1.x, are broken and select() says that ** O_NDELAY sockets are always writable even when ** they're actually not. ** ** Current workaround is to block sending on this ** socket for a brief adaptively-tuned period. ** Fortunately we already have all the necessary ** blocking code, for use with throttling. */ c->wouldblock_delay += MIN_WOULDBLOCK_DELAY; c->conn_state = CNST_PAUSING; fdwatch_del_fd( hc->conn_fd ); client_data.p = c; if ( c->wakeup_timer != (Timer*) 0 ) syslog( LOG_ERR, "replacing non-null wakeup_timer!" ); c->wakeup_timer = tmr_create( tvP, wakeup_connection, client_data, c->wouldblock_delay, 0 ); if ( c->wakeup_timer == (Timer*) 0 ) { syslog( LOG_CRIT, "tmr_create(wakeup_connection) failed" ); exit( 1 ); } return; } if ( sz < 0 ) { /* Something went wrong, close this connection. ** ** If it's just an EPIPE, don't bother logging, that ** just means the client hung up on us. ** ** On some systems, write() occasionally gives an EINVAL. ** Dunno why, something to do with the socket going ** bad. Anyway, we don't log those either. ** ** And ECONNRESET isn't interesting either. */ if ( errno != EPIPE && errno != EINVAL && errno != ECONNRESET ) syslog( LOG_ERR, "write - %m sending %.80s", hc->encodedurl ); clear_connection( c, tvP ); return; } /* Ok, we wrote something. */ c->active_at = tvP->tv_sec; /* Was this a headers + file writev()? */ if ( hc->responselen > 0 ) { /* Yes; did we write only part of the headers? */ if ( sz < hc->responselen ) { /* Yes; move the unwritten part to the front of the buffer. */ int newlen = hc->responselen - sz; (void) memmove( hc->response, &(hc->response[sz]), newlen ); hc->responselen = newlen; sz = 0; } else { /* Nope, we wrote the full headers, so adjust accordingly. */ sz -= hc->responselen; hc->responselen = 0; } } /* And update how much of the file we wrote. */ c->next_byte_index += sz; c->hc->bytes_sent += sz; for ( tind = 0; tind < c->numtnums; ++tind ) throttles[c->tnums[tind]].bytes_since_avg += sz; /* Are we done? */ if ( c->next_byte_index >= c->end_byte_index ) { /* This connection is finished! */ finish_connection( c, tvP ); return; } /* Tune the (blockheaded) wouldblock delay. */ if ( c->wouldblock_delay > MIN_WOULDBLOCK_DELAY ) c->wouldblock_delay -= MIN_WOULDBLOCK_DELAY; /* If we're throttling, check if we're sending too fast. */ if ( c->max_limit != THROTTLE_NOLIMIT ) { elapsed = tvP->tv_sec - c->started_at; if ( elapsed == 0 ) elapsed = 1; /* count at least one second */ if ( c->hc->bytes_sent / elapsed > c->max_limit ) { c->conn_state = CNST_PAUSING; fdwatch_del_fd( hc->conn_fd ); /* How long should we wait to get back on schedule? If less ** than a second (integer math rounding), use 1/2 second. */ coast = c->hc->bytes_sent / c->max_limit - elapsed; client_data.p = c; if ( c->wakeup_timer != (Timer*) 0 ) syslog( LOG_ERR, "replacing non-null wakeup_timer!" ); c->wakeup_timer = tmr_create( tvP, wakeup_connection, client_data, coast > 0 ? ( coast * 1000L ) : 500L, 0 ); if ( c->wakeup_timer == (Timer*) 0 ) { syslog( LOG_CRIT, "tmr_create(wakeup_connection) failed" ); exit( 1 ); } } } /* (No check on min_limit here, that only controls connection startups.) */ } static void handle_linger( connecttab* c, struct timeval* tvP ) { char buf[4096]; int r; /* In lingering-close mode we just read and ignore bytes. An error ** or EOF ends things, otherwise we go until a timeout. */ r = read( c->hc->conn_fd, buf, sizeof(buf) ); if ( r < 0 && ( errno == EINTR || errno == EAGAIN ) ) return; if ( r <= 0 ) really_clear_connection( c, tvP ); } static int check_throttles( connecttab* c ) { int tnum; long l; c->numtnums = 0; c->max_limit = c->min_limit = THROTTLE_NOLIMIT; for ( tnum = 0; tnum < numthrottles && c->numtnums < MAXTHROTTLENUMS; ++tnum ) if ( match( throttles[tnum].pattern, c->hc->expnfilename ) ) { /* If we're way over the limit, don't even start. */ if ( throttles[tnum].rate > throttles[tnum].max_limit * 2 ) return 0; /* Also don't start if we're under the minimum. */ if ( throttles[tnum].rate < throttles[tnum].min_limit ) return 0; if ( throttles[tnum].num_sending < 0 ) { syslog( LOG_ERR, "throttle sending count was negative - shouldn't happen!" ); throttles[tnum].num_sending = 0; } c->tnums[c->numtnums++] = tnum; ++throttles[tnum].num_sending; l = throttles[tnum].max_limit / throttles[tnum].num_sending; if ( c->max_limit == THROTTLE_NOLIMIT ) c->max_limit = l; else c->max_limit = MIN( c->max_limit, l ); l = throttles[tnum].min_limit; if ( c->min_limit == THROTTLE_NOLIMIT ) c->min_limit = l; else c->min_limit = MAX( c->min_limit, l ); } return 1; } static void clear_throttles( connecttab* c, struct timeval* tvP ) { int tind; for ( tind = 0; tind < c->numtnums; ++tind ) --throttles[c->tnums[tind]].num_sending; } static void update_throttles( ClientData client_data, struct timeval* nowP ) { int tnum, tind; int cnum; connecttab* c; long l; /* Update the average sending rate for each throttle. This is only used ** when new connections start up. */ for ( tnum = 0; tnum < numthrottles; ++tnum ) { throttles[tnum].rate = ( 2 * throttles[tnum].rate + throttles[tnum].bytes_since_avg / THROTTLE_TIME ) / 3; throttles[tnum].bytes_since_avg = 0; /* Log a warning message if necessary. */ if ( throttles[tnum].rate > throttles[tnum].max_limit && throttles[tnum].num_sending != 0 ) { if ( throttles[tnum].rate > throttles[tnum].max_limit * 2 ) syslog( LOG_NOTICE, "throttle #%d '%.80s' rate %ld greatly exceeding limit %ld; %d sending", tnum, throttles[tnum].pattern, throttles[tnum].rate, throttles[tnum].max_limit, throttles[tnum].num_sending ); else syslog( LOG_INFO, "throttle #%d '%.80s' rate %ld exceeding limit %ld; %d sending", tnum, throttles[tnum].pattern, throttles[tnum].rate, throttles[tnum].max_limit, throttles[tnum].num_sending ); } if ( throttles[tnum].rate < throttles[tnum].min_limit && throttles[tnum].num_sending != 0 ) { syslog( LOG_NOTICE, "throttle #%d '%.80s' rate %ld lower than minimum %ld; %d sending", tnum, throttles[tnum].pattern, throttles[tnum].rate, throttles[tnum].min_limit, throttles[tnum].num_sending ); } } /* Now update the sending rate on all the currently-sending connections, ** redistributing it evenly. */ for ( cnum = 0; cnum < max_connects; ++cnum ) { c = &connects[cnum]; if ( c->conn_state == CNST_SENDING || c->conn_state == CNST_PAUSING ) { c->max_limit = THROTTLE_NOLIMIT; for ( tind = 0; tind < c->numtnums; ++tind ) { tnum = c->tnums[tind]; l = throttles[tnum].max_limit / throttles[tnum].num_sending; if ( c->max_limit == THROTTLE_NOLIMIT ) c->max_limit = l; else c->max_limit = MIN( c->max_limit, l ); } } } } static void finish_connection( connecttab* c, struct timeval* tvP ) { /* If we haven't actually sent the buffered response yet, do so now. */ httpd_write_response( c->hc ); /* And clear. */ clear_connection( c, tvP ); } static void clear_connection( connecttab* c, struct timeval* tvP ) { ClientData client_data; if ( c->wakeup_timer != (Timer*) 0 ) { tmr_cancel( c->wakeup_timer ); c->wakeup_timer = 0; } /* This is our version of Apache's lingering_close() routine, which is ** their version of the often-broken SO_LINGER socket option. For why ** this is necessary, see http://www.apache.org/docs/misc/fin_wait_2.html ** What we do is delay the actual closing for a few seconds, while reading ** any bytes that come over the connection. However, we don't want to do ** this unless it's necessary, because it ties up a connection slot and ** file descriptor which means our maximum connection-handling rate ** is lower. So, elsewhere we set a flag when we detect the few ** circumstances that make a lingering close necessary. If the flag ** isn't set we do the real close now. */ if ( c->conn_state == CNST_LINGERING ) { /* If we were already lingering, shut down for real. */ tmr_cancel( c->linger_timer ); c->linger_timer = (Timer*) 0; c->hc->should_linger = 0; } if ( c->hc->should_linger ) { if ( c->conn_state != CNST_PAUSING ) fdwatch_del_fd( c->hc->conn_fd ); c->conn_state = CNST_LINGERING; shutdown( c->hc->conn_fd, SHUT_WR ); fdwatch_add_fd( c->hc->conn_fd, c, FDW_READ ); client_data.p = c; if ( c->linger_timer != (Timer*) 0 ) syslog( LOG_ERR, "replacing non-null linger_timer!" ); c->linger_timer = tmr_create( tvP, linger_clear_connection, client_data, LINGER_TIME, 0 ); if ( c->linger_timer == (Timer*) 0 ) { syslog( LOG_CRIT, "tmr_create(linger_clear_connection) failed" ); exit( 1 ); } } else really_clear_connection( c, tvP ); } static void really_clear_connection( connecttab* c, struct timeval* tvP ) { stats_bytes += c->hc->bytes_sent; if ( c->conn_state != CNST_PAUSING ) fdwatch_del_fd( c->hc->conn_fd ); httpd_close_conn( c->hc, tvP ); clear_throttles( c, tvP ); if ( c->linger_timer != (Timer*) 0 ) { tmr_cancel( c->linger_timer ); c->linger_timer = 0; } c->conn_state = CNST_FREE; c->next_free_connect = first_free_connect; first_free_connect = c - connects; /* division by sizeof is implied */ --num_connects; } static void idle( ClientData client_data, struct timeval* nowP ) { int cnum; connecttab* c; for ( cnum = 0; cnum < max_connects; ++cnum ) { c = &connects[cnum]; switch ( c->conn_state ) { case CNST_READING: if ( nowP->tv_sec - c->active_at >= IDLE_READ_TIMELIMIT ) { syslog( LOG_INFO, "%.80s connection timed out reading", httpd_ntoa( &c->hc->client_addr ) ); httpd_send_err( c->hc, 408, httpd_err408title, "", httpd_err408form, "" ); finish_connection( c, nowP ); } break; case CNST_SENDING: case CNST_PAUSING: if ( nowP->tv_sec - c->active_at >= IDLE_SEND_TIMELIMIT ) { syslog( LOG_INFO, "%.80s connection timed out sending", httpd_ntoa( &c->hc->client_addr ) ); clear_connection( c, nowP ); } break; } } } static void wakeup_connection( ClientData client_data, struct timeval* nowP ) { connecttab* c; c = (connecttab*) client_data.p; c->wakeup_timer = (Timer*) 0; if ( c->conn_state == CNST_PAUSING ) { c->conn_state = CNST_SENDING; fdwatch_add_fd( c->hc->conn_fd, c, FDW_WRITE ); } } static void linger_clear_connection( ClientData client_data, struct timeval* nowP ) { connecttab* c; c = (connecttab*) client_data.p; c->linger_timer = (Timer*) 0; really_clear_connection( c, nowP ); } static void occasional( ClientData client_data, struct timeval* nowP ) { mmc_cleanup( nowP ); tmr_cleanup(); watchdog_flag = 1; /* let the watchdog know that we are alive */ } #ifdef STATS_TIME static void show_stats( ClientData client_data, struct timeval* nowP ) { logstats( nowP ); } #endif /* STATS_TIME */ /* Generate debugging statistics syslog messages for all packages. */ static void logstats( struct timeval* nowP ) { struct timeval tv; time_t now; long up_secs, stats_secs; if ( nowP == (struct timeval*) 0 ) { (void) gettimeofday( &tv, (struct timezone*) 0 ); nowP = &tv; } now = nowP->tv_sec; up_secs = now - start_time; stats_secs = now - stats_time; if ( stats_secs == 0 ) stats_secs = 1; /* fudge */ stats_time = now; syslog( LOG_NOTICE, "up %ld seconds, stats for %ld seconds:", up_secs, stats_secs ); thttpd_logstats( stats_secs ); httpd_logstats( stats_secs ); mmc_logstats( stats_secs ); fdwatch_logstats( stats_secs ); tmr_logstats( stats_secs ); } /* Generate debugging statistics syslog message. */ static void thttpd_logstats( long secs ) { if ( secs > 0 ) syslog( LOG_NOTICE, " thttpd - %ld connections (%g/sec), %d max simultaneous, %lld bytes (%g/sec), %d httpd_conns allocated", stats_connections, (float) stats_connections / secs, stats_simultaneous, (long long) stats_bytes, (float) stats_bytes / secs, httpd_conn_count ); stats_connections = 0; stats_bytes = 0; stats_simultaneous = 0; }