/* * Drood Copyright (c) 2007, James Bailie. * 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 name of James Bailie 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 HOLDER 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. */ /* * Drood is a minimal, multiplexing, kqueue-based, HTTP/1.1 server. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define HASH_SIZE 256 #define VERSION "1.16" #define PIDFILE "/var/run/drood.pid" /* * Size of the output queue used by kevent(). */ #define QLEN 1024 /* * This is size of the IO buffers used in connection control blocks. * Modifying this value may require a corresponding modification to * the precision of the chunk header conversion performed in * respond_chunked(). 32K is the default size of a socket send buffer * on FreeBSD 6.x. We try to move data in chunks of this size in an * attempt to "fill the pipe" of each connection. */ #define IOBUFSIZE 32768 /* * A connection is in one of the following states at any one time. There * are handlers for each state which are invoked when kevent() returns an * event on a connection. A connection may have the handler for its * current state called more than once as part of the multiplexing process. * Eventually a handler will advance a connection to a succeeding state, * unless a system error occurs, or the connection times-out, when it will * be dropped. Any connection in a state described as a terminal state, * will transition back to the RECEIVE state for a persistent connection, * otherwise the connection will be closed. * RECEIVE: Receive request header. Transitions to ANALYZE or ERROR. * ANALYZE: Validate request, open file or fork CGI program. Transitions * to HEADER for static resource, FILTER for dynamic, or ERROR. * HEADER: Send response header. Is terminal state for HEAD requests and * CGI redirects. When a response body follows the header, * transitions to RESPOND, NOCHUNK, CHUNK, or CONTINUE. * RESPOND: Send response body for static resource, * Invoked at least once for each IOBUFSIZE portion of requested * resource. Terminal state. * NOCHUNK: Send a response body from a CGI program which either provided * a Content-Length header, or was invoked by an HTTP/1.0 request. * Invoked at least once for every IOBUFSIZE protion of the CGI * program's output. * CHUNK: Chunk a response body on-the-fly for an HTTP/1.1 CGI * request. Invoked at least once for each IOBUFSIZE - 8 * portion of the CGI program's output. Terminal state. * CONTINUE: Continue processing a POST request which contained an * "Expect: continue" header, after "100 Continue" has been sent. * ERROR: Send an error response header and body. * Connections may transition here from states RECEIVE, ANALYZE, * and FILTER. Terminal state. * FILTER: Read and supplement a CGI response header, as * necessary. Transitions to ERROR or HEADER. */ #define RECEIVE 0 #define ANALYZE 1 #define RESPOND 2 #define FILTER 3 #define CHUNK 4 #define NOCHUNK 5 #define ERROR 6 #define HEADER 7 #define CONTINUE 8 /* * These must start at 1. 0 means "no error". */ #define ERR_BADVERSION 1 #define ERR_BADREQUEST 2 #define ERR_FORBIDDEN 3 #define ERR_LENGTHREQ 4 #define ERR_BADMETHOD 5 #define ERR_NOTFOUND 6 #define ERR_INTERNAL 7 #define ERR_NOCONTENT 8 #define ERR_NOTMODIFIED 9 /* * Input queue. */ #define INQ 16 struct kevent inqueue[ INQ ]; int in = 0, closed = 0; /* * Number of seconds after which an idle connection is dropped. */ int read_timeout = 5; /* * connection control block. */ struct ccb { int type, verno, error, next, state, close, expect, count, source, sock, inactive; off_t size, written; char *request, *method, *agent, *cookie, *host, *length, *version, *addr, *since; char buffer[ IOBUFSIZE ]; struct stack *working; }; void receive_request( struct ccb * ); void analyze_request( struct ccb * ); void respond_static( struct ccb * ); void filter_cgi_header( struct ccb * ); void respond_chunked( struct ccb * ); void respond_unchunked( struct ccb * ); void respond_error( struct ccb * ); void respond_header( struct ccb * ); void prepare_dynamic( struct ccb * ); int uts = -1; struct utsname un; /* * Table of pointers to handlers for each of the states a connection may be * in. */ void ( *handlers[ 9 ] )( struct ccb * ); extern char *optarg; char *root_resource = "index.html"; volatile int sigterm = 0; int fd = -1, increment = 1024, notready = 0, nolog = 0, matched = 0, logging = 0, testing = 0, do_chroot = 0, backlog = 100; struct passwd *passwd; struct group *group; char *interface = "127.0.0.1", *port = "80", *dynamic = NULL, *grp = "nobody", *user = "nobody", *root = NULL, *default_type = "text/plain; charset=utf-8"; char *errstrings[ 10 ] = { "", " 505 HTTP Version Not Supported\r\n", " 400 Bad Request\r\n", " 403 Forbidden\r\n", " 411 Length Required\r\n", " 405 Method Not Allowed\r\nAllow: GET, HEAD, POST\r\n", " 404 Not Found\r\n", " 500 Internal Server Error\r\n", " 204 No Content\r\n", " 304 Not Modified\r\n" }; /* * If a filename's suffix isn't listed here, the file its type is * considered to be text/plain. */ char *type_table[] = { "html", "text/html; charset=utf-8", "htm", "text/html; charset=utf-8", "xml", "text/xml; charset-utf-8", "txt", "text/plain; charset=utf-8", "css", "text/css", "ico", "image/vnd.microsoft.icon", "png", "image/png", "jpg", "image/jpeg", "jpeg", "image/jpeg", "tiff", "image/tiff", "tif", "image/tiff", "gif", "image/gif", "eps", "application/postscript", "ps", "application/postscript", "gz", "application/x-gzip", "tgz", "application/x-compressed", "zip", "application/zip", "pdf", "application/pdf", "swf", "application/x-shockwave-flash", "js", "application/x-javascript", "mpg", "video/mpeg", "mpa", "video/mpeg", "mpe", "video/mpeg", "mp2", "video/mpeg", "mov", "video/quicktime", "qt", "video/quicktime", "avi", "video/x-msvideo", "asf", "video/x-ms-asf", "asx", "video/x-ms-asf", "asr", "video/x-ms-asf", "flv", "video/x-flv", "ra", "audio/x-pn-realaudio", "ram", "audio/x-pn-realaudio", "wav", "audio/x-wav", "mp3", "audio/mpeg", "" }; regex_t request_rx, length_rx, host_rx, agent_rx, cookie_rx, close_rx, expect_rx, location_rx, date_rx, parent_rx, hostcheck_rx, suffix_rx, static_rx, dynamic_rx, type_rx, since_rx; /* * String which grows dynamically. */ struct string { int free, used; char *top; char *str; }; /* * Stack which grows dynamically. */ struct stack { int free, used; char **top, **values; } *hosts = NULL; void ev_set( int desc, short filter, u_short flags, struct ccb *item ) { struct kevent *kev; if ( in >= INQ ) return; kev = &inqueue[ in++ ]; kev->ident = desc; kev->filter = filter; kev->fflags = 0; kev->flags = flags; kev->udata = item; } void flip_events( struct ccb *item ) { if ( item->source >= 0 ) ev_set( item->source, EVFILT_READ, EV_DISABLE, item ); ev_set( item->sock, EVFILT_READ, EV_DISABLE, item ); ev_set( item->sock, EVFILT_WRITE, EV_ENABLE, item ); } void delete_timer( struct ccb *item ) { struct kevent *kev; if ( in >= INQ ) return; kev = &inqueue[ in++ ]; kev->ident = item->sock; kev->filter = EVFILT_TIMER; kev->fflags = 0; kev->data = 0; kev->flags = EV_DELETE; kev->udata = item; } void set_timer( struct ccb *item ) { struct kevent *kev; if ( in >= INQ ) return; kev = &inqueue[ in++ ]; kev->ident = item->sock; kev->filter = EVFILT_TIMER; kev->fflags = 0; kev->data = read_timeout * 1000; kev->flags = EV_ADD; kev->udata = item; } void non_blocking( int desc ) { int flags, unblocked; if (( flags = fcntl( desc, F_GETFL, 0 )) < 0 ) { syslog( LOG_ERR, "fcntl(): %m" ); exit( 1 ); } unblocked = flags & O_NONBLOCK; if ( ! unblocked && fcntl( desc, F_SETFL, flags | O_NONBLOCK ) < 0 ) { syslog( LOG_ERR, "fcntl(): %m" ); exit( 1 ); } } void *memory( int size ) { void *ptr; if ( size == 0 ) return NULL; if (( ptr = malloc( size )) == NULL ) { if ( logging ) syslog( LOG_WARNING, "malloc(): %m" ); else fprintf( stderr, "drood: malloc(): %s\n", strerror( errno )); } return ptr; } struct stack *make_stack() { struct stack *a; if (( a = ( struct stack *)memory( sizeof( struct stack ))) == NULL ) return NULL; if (( a->values = memory( sizeof( void * ) * increment )) == NULL ) return NULL; a->free = increment; a->used = 0; a->top = a->values; return a; } void stack_free( struct stack *a ) { free( a->values ); free( a ); } char *stack_push( struct stack *a, char *o ) { if ( a->free == 0 ) { a->values = realloc( a->values, sizeof( void * ) * ( a->used + increment ) ); if ( a->values == NULL ) { if ( logging ) syslog( LOG_WARNING, "realloc(): %m" ); else fprintf( stderr, "drood: realloc(): %s\n", strerror( errno )); return NULL; } a->free = increment; a->top = &a->values[ a->used - 1 ]; } if ( a->used ) ++a->top; *a->top = o; --a->free; ++a->used; return o; } char *stack_pop( struct stack *a ) { char *ptr; if ( a->used ) { ptr = *a->top; --a->used; ++a->free; if ( a->used ) --a->top; return ptr; } return NULL; } char *str_dup( char *str, int len ) { char *ptr; if (( ptr = ( char *)memory( len + 1 )) != NULL ) { bcopy( str, ptr, len ); ptr[ len ] = '\0'; } return ptr; } struct string *make_string() { struct string *s; s = ( struct string *)memory( sizeof( struct string )); if ( s == NULL ) return s; s->str = ( char *)memory( increment + 1 ); *s->str = '\0'; s->free = increment; s->used = 0; s->top = s->str; return s; } void string_free( struct string *s ) { free( s->str ); free( s ); } void sigterm_handler( int signo ) { sigterm = 1; } struct string *string_append( struct string *s, char c ) { if ( s->free == 0 ) { s->str = ( char *)realloc( s->str, s->used + 1 + increment ); if ( s->str == NULL ) { if ( logging ) syslog( LOG_WARNING, "realloc(): %m" ); else fprintf( stderr, "drood: realloc(): %s\n", strerror( errno )); return NULL; } /* Leave room for end-of-string sentinel */ s->free = increment; s->top = &s->str[ s->used ]; } ++s->used; --s->free; *s->top++ = c; *s->top = '\0'; return s; } void init_var( int argc, char **argv ) { DIR *dir; struct dirent *dp; char *ptr; int i; while(( i = getopt( argc, argv, "nxcq:t:i:p:r:d:u:g:e:")) != -1 ) switch( i ) { case 'c': do_chroot = 1; break; case 'e': root_resource = ( optarg == NULL ? "index.html" : optarg ); break; case 'g': grp = ( optarg == NULL ? "nobody" : optarg ); break; case 'i': interface = ( optarg == NULL ? "127.0.0.1" : optarg ); break; case 'n': nolog = 1; break; case 'p': port = ( optarg == NULL ? "80" : optarg ); break; case 'r': root = optarg; break; case 't': read_timeout = ( optarg == NULL ? 0 : atoi( optarg )); break; case 'd': dynamic = optarg; break; case 'u': user = ( optarg == NULL ? "nobody" : optarg ); break; case 'q': backlog = ( optarg == NULL ? 100 : atoi( optarg )); break; case 'x': testing = 1; } if ( root == NULL ) { fputs( "drood: server root directory not specified\n", stderr ); exit( 1 ); } if ( read_timeout < 1 || read_timeout > 60 ) { fprintf( stderr, "drood: read timeout value %d out of range\n", read_timeout ); exit( 1 ); } if (( passwd = getpwnam( user )) == NULL ) { fprintf( stderr, "drood: user \"%s\" does not exist", user ); exit( 1 ); } if (( group = getgrnam( grp )) == NULL ) { fprintf( stderr, "drood: group \"%s\" does not exist", grp ); exit( 1 ); } if (( hosts = make_stack()) == NULL ) exit( 1 ); if (( dir = opendir( root )) == NULL ) { fprintf( stderr, "drood: %s cannot be opened\n", root ); exit( 1 ); } while(( dp = readdir( dir )) != NULL ) { if ( dynamic != NULL && ! strcmp( dp->d_name, dynamic )) continue; if(( ptr = str_dup( dp->d_name, dp->d_namlen )) == NULL ) exit( 1 ); stack_push( hosts, ptr ); } closedir( dir ); handlers[ 0 ] = receive_request; handlers[ 1 ] = analyze_request; handlers[ 2 ] = respond_static; handlers[ 3 ] = filter_cgi_header; handlers[ 4 ] = respond_chunked; handlers[ 5 ] = respond_unchunked; handlers[ 6 ] = respond_error; handlers[ 7 ] = respond_header; handlers[ 8 ] = prepare_dynamic; if( ! ( uts = uname( &un )) ) un.release[ 3 ] = '\0'; } void failed_rx( int result, regex_t *rx ) { char err[ 80 ]; regerror( result, rx, err, sizeof( err )); fprintf( stderr, "drood: regcomp(): %s\n", err ); exit( 1 ); } void init_rx() { int flags, result; char *ptr; struct string *s; flags = REG_EXTENDED | REG_ICASE; if (( result = regcomp( &request_rx, "^([^ \t:]+)[ \t]+([^ \t]+)[ \t]+(HTTP/[0-9]\\.[0-9])$", flags ))) failed_rx( result, &request_rx ); if (( result = regcomp( &length_rx, "^Content-Length:[\t ]+([^ \t]+)", flags ))) failed_rx( result, &length_rx ); if (( result = regcomp( &type_rx, "^Content-Type:[\t ]+[^ \t]+", flags ))) failed_rx( result, &type_rx ); if (( result = regcomp( &host_rx, "^Host:[ \t]+([^\t ]+)", flags ))) failed_rx( result, &host_rx ); if (( result = regcomp( &hostcheck_rx, "^(\\.\\.?|.*/.*)$", flags ))) failed_rx( result, &hostcheck_rx ); if (( result = regcomp( &cookie_rx, "^Cookie:[ \t]+(.+)", flags ))) failed_rx( result, &cookie_rx ); if (( result = regcomp( &agent_rx, "^User-Agent:[ \t]+(.+)", flags ))) failed_rx( result, &agent_rx ); if (( result = regcomp( &close_rx, "^Connection:[ \t]+close", flags ))) failed_rx( result, &close_rx ); if (( result = regcomp( &expect_rx, "^Expect:[ \t]+100-continue", flags ))) failed_rx( result, &expect_rx ); if (( result = regcomp( &location_rx, "^Location:[ \t]+[^\t ]+", flags ))) failed_rx( result, &location_rx ); if (( result = regcomp( &date_rx, "^Date:[ \t]+[^ \t]+", flags ))) failed_rx( result, &date_rx ); if (( result = regcomp( &parent_rx, "(^|/)\\.\\./", flags ))) failed_rx( result, &parent_rx ); if (( result = regcomp( &suffix_rx, "\\.([^. \t]+$)", flags ))) failed_rx( result, &suffix_rx ); if (( result = regcomp( &since_rx, "^If-Modified-Since:[\t ]+(.+)$", flags ))) failed_rx( result, &since_rx ); if ( dynamic != NULL ) { if (( s = make_string() ) == NULL ) exit( 1 ); if ( string_append( s, '/' ) == NULL ) exit( 1 ); for( ptr = dynamic; *ptr; ++ptr ) if ( string_append( s, *ptr ) == NULL ) exit( 1 ); if ( string_append( s, '/' ) == NULL ) exit( 1 ); if (( result = regcomp( &dynamic_rx, s->str, REG_NOSPEC ))) failed_rx( result, &dynamic_rx ); string_free( s ); } } void become_daemon() { struct sigaction sigact; /* * Fork and led the parent die, continuing as child so we are not * a process group leader. This is necessary for the call to setsid(). */ switch( fork() ) { case -1: fprintf( stderr, "drood: fork(): %s\n", strerror( errno )); exit( 1 ); case 0: break; default: exit( 0 ); } openlog( "drood", LOG_PID, LOG_DAEMON ); logging = 1; signal( SIGHUP, SIG_IGN ); signal( SIGTTIN, SIG_IGN ); signal( SIGTTOU, SIG_IGN ); signal( SIGTSTP, SIG_IGN ); signal( SIGINT, SIG_IGN ); signal( SIGQUIT, SIG_IGN ); signal( SIGUSR1, SIG_IGN ); signal( SIGUSR2, SIG_IGN ); signal( SIGALRM, SIG_IGN ); sigact.sa_handler = sigterm_handler; sigemptyset( &sigact.sa_mask ); sigact.sa_flags = 0; if ( sigaction( SIGTERM, &sigact, NULL ) < 0 ) { fprintf( stderr, "sigaction(): %s\n", strerror( errno )); exit( 1 ); } fclose( stdout ); fclose( stderr ); fclose( stdin ); stdin = fopen( "/dev/null", "r" ); stdout = fopen( "/dev/null", "w" ); stderr = fopen( "/dev/null", "w" ); if ( stdin == NULL || stdout == NULL || stderr == NULL ) { syslog( LOG_ERR, "fopen(): %m" ); exit( 1 ); } if ( setsid() < 0 ) { syslog( LOG_ERR, "setsid(): %m" ); exit( 1 ); } umask( 0 ); } void start_listening() { struct sockaddr_in serv_addr; int flags = 1, file; if ( testing ) { openlog( "drood", LOG_PID, LOG_DAEMON ); logging = 1; } fd = socket( PF_INET, SOCK_STREAM, 0 ); if ( fd == -1 ) { syslog( LOG_ERR, "socket(): %m" ); exit( 1 ); } if ( setsockopt( fd, SOL_SOCKET, SO_REUSEADDR, &flags, sizeof( flags )) < 0 ) syslog( LOG_WARNING, "setsockopt( SO_REUSEADDR ): %m" ); if ( setsockopt( fd, SOL_SOCKET, SO_KEEPALIVE, &flags, sizeof( flags )) < 0 ) syslog( LOG_WARNING, "setsockopt( SO_KEEPALIVE ): %m" ); bzero( &serv_addr, sizeof( serv_addr )); serv_addr.sin_family = AF_INET; if ( ! inet_aton( interface, ( struct in_addr *)&serv_addr.sin_addr.s_addr )) { syslog( LOG_ERR, "inet_aton(): %m" ); exit( 1 ); } serv_addr.sin_port = htons( atoi( port )); if ( bind( fd, ( struct sockaddr *)&serv_addr, sizeof( serv_addr )) < 0 ) { syslog( LOG_ERR, "bind(): %m" ); exit( 1 ); } if ( listen( fd, backlog ) < 0 ) { syslog( LOG_ERR, "listen(): %m" ); exit( 1 ); } if ( ! testing ) { if (( file = open( PIDFILE, O_WRONLY | O_CREAT | O_TRUNC )) < 0 ) syslog( LOG_WARNING, "open(): %m" ); else { char buffer[ 16 ]; snprintf( buffer, sizeof( buffer ), "%d", getpid() ); write( file, buffer, strlen( buffer )); close( file ); } } /* * We do not use accept filters here because they prevent drood from * timing out an inactive connection when reading requests. */ if ( chdir( root ) < 0 ) { syslog( LOG_ERR, "chdir(): %m" ); exit( 1 ); } if ( do_chroot && chroot( root ) < 0 ) { syslog( LOG_ERR, "chroot(): %m" ); exit( 1 ); } if ( setgid( group->gr_gid ) < 0 ) { syslog( LOG_ERR, "setgid(): %m" ); exit( 1 ); } if ( setuid( passwd->pw_uid ) < 0 ) { syslog( LOG_ERR, "setuid(): %m" ); exit( 1 ); } /* * SIGPIPE and SIGCHLD are set to be ignored here, and not in * become_daemon() so that these signals will be ignored even if the * user provides the -x option to the server, preventing become_daemon() * from being invoked. */ signal( SIGPIPE, SIG_IGN ); /* * On FreeBSD, setting SIGCHLD to SIG_IGN prevents zombies from * occurring. Entries in the process table are removed by the kernel * when the associated process exits. */ signal( SIGCHLD, SIG_IGN ); } void add_conn( int new ) { struct ccb *ptr; /* * Drop the connection if we have run out of memory. */ if (( ptr = memory( sizeof( struct ccb ))) == NULL ) { close( new ); return; } ptr->working = NULL; *ptr->buffer = '\0'; ptr->close = ptr->expect = ptr->count = ptr->error = ptr->next = ptr->inactive = 0; ptr->size = ptr->written = 0; ptr->state = RECEIVE; ptr->source = ptr->verno = ptr->type = -1; ptr->sock = new; ptr->host = ptr->length = ptr->version = ptr->agent = ptr->cookie = ptr->request = ptr->method = ptr->addr = ptr->since = NULL; ev_set( ptr->sock, EVFILT_READ, EV_ADD, ptr ); ev_set( ptr->sock, EVFILT_WRITE, EV_ADD | EV_DISABLE, ptr ); set_timer( ptr ); } void clear_conn( struct ccb *item ) { char *ptr; if ( item->source >= 0 ) { close( item->source ); item->source = -1; } if ( item->host != NULL ) { free( item->host ); item->host = NULL; } if ( item->length != NULL ) { free( item->length ); item->length = NULL; } if ( item->version != NULL ) { free( item->version ); item->version = NULL; } if ( item->agent != NULL ) { free( item->agent ); item->agent = NULL; } if ( item->cookie != NULL ) { free( item->cookie ); item->cookie = NULL; } if ( item->request != NULL ) { free( item->request ); item->request = NULL; } if ( item->method != NULL ) { free( item->method ); item->method = NULL; } if ( item->addr != NULL ) { free( item->addr ); item->addr = NULL; } if ( item->since != NULL ) { free( item->since ); item->since = NULL; } if ( item->working != NULL ) { while(( ptr = ( char *)stack_pop( item->working )) != NULL ) free( ptr ); } *item->buffer = '\0'; item->close = item->count = item->error = item->expect = item->next = item->inactive = 0; item->size = item->written = 0; item->state = RECEIVE; item->type = item->verno = -1; if ( item->sock >= 0 ) { ev_set( item->sock, EVFILT_READ, EV_ENABLE, item ); ev_set( item->sock, EVFILT_WRITE, EV_DISABLE , item ); set_timer( item ); } return; } void remove_conn( struct ccb *item ) { if ( item->sock < 0 ) return; closed = item->sock; delete_timer( item ); close( item->sock ); item->sock = -1; clear_conn( item ); if ( item->working != NULL ) stack_free( item->working ); free( item ); return; } char **match( regex_t *regexp, char *the_string, int len, int subexpr ) { static struct string *buffer = NULL; static char *results[ 3 ]; regmatch_t matches[ 4 ]; int result, length, i, j, k; if ( buffer == NULL && ( buffer = make_string() ) == NULL ) return NULL; matches[ 0 ].rm_so = 0; matches[ 0 ].rm_eo = len; result = regexec( regexp, the_string, ( subexpr ? 4 : 0 ), matches, REG_STARTEND ); if ( result ) { char err[ 80 ]; if ( result == REG_NOMATCH ) { matched = 0; return NULL; } regerror( result, regexp, err, sizeof( err )); syslog( LOG_ERR, "regexec: %m" ); exit( 1 ); } matched = 1; results[ 0 ] = results[ 1 ] = results[ 2 ] = NULL; if ( ! subexpr ) return results; for( k = 0, i = 1; i < 4; ++i, ++k ) { buffer->free += buffer->used; buffer->used = 0; buffer->top = buffer->str; length = 0; if ( matches[ i ].rm_so >= 0 ) { length = matches[ i ].rm_so + matches[ i ].rm_eo - matches[ i ].rm_so; for( j = matches[ i ].rm_so; j < length; ++j ) if ( string_append( buffer, the_string[ j ] ) == NULL ) { if ( k ) { free( results[ 0 ] ); if ( k == 2 ) free( results[ 1 ] ); } return NULL; } } if ( matches[ i ].rm_so >= 0 && length ) { if (( results[ k ] = str_dup( buffer->str, buffer->used )) == NULL ) { if ( k ) { free( results[ 0 ] ); if ( k == 2 ) free( results[ 1 ] ); } return NULL; } } else results[ k ] = NULL; } return results; } char *get_header_line( struct ccb *item, int sock ) { int r; char c; int maxlen = IOBUFSIZE - 100; notready = 0; if ( ! item->inactive ) { item->count = 0; *item->buffer = '\0'; } for( ; ; ) { r = read( ( sock ? item->sock : item->source ), &c, 1 ); if ( r <= 0 ) { if ( sigterm ) return NULL; if ( errno == EWOULDBLOCK || errno == EAGAIN ) notready = 1; return NULL; } /* * We don't want the line terminators. */ if ( c == 13 ) continue; if ( c == 10 ) break; /* * New character overwrites previous string terminator. New string * terminator added. maxlen is always 100 characters less than the * IOBUFSIZE so we do not have to worry about buffer overflow. * Notice that item->count does not count the string terminator. */ item->buffer[ item->count++ ] = c, item->buffer[ item->count ] = '\0'; if ( item->count > maxlen ) { notready = 2; return NULL; } } return item->buffer; } void receive_request( struct ccb *item ) { char *line; char **matches; for( ; ; ) { if (( line = get_header_line( item, 1 )) == NULL ) { switch( notready ) { /* * 0 means premature EOF or we received a SIGTERM. */ case 0: remove_conn( item ); return; /* * 1 means the connection has become inactive. */ case 1: item->inactive = 1; return; /* * 2 means the client sent a too-long line. If this happens, * there is most likely malicious intent, and the request header * is now incomplete, so we drop the connection after sending * the error message. */ case 2: item->count = 0; item->written = 0; item->close = 1; item->error = ERR_BADREQUEST; item->state = ERROR; delete_timer( item ); flip_events( item ); return; } } item->inactive = 0; /* * If we have the request header separator line, we are done here. */ if ( *line == '\0' ) { delete_timer( item ); flip_events( item ); if ( item->request == NULL || item->method == NULL || item->version == NULL ) { item->count = 0; item->written = 0; item->state = ERROR; item->error = ERR_BADREQUEST; return; } item->count = 0; item->written = 0; item->state = ANALYZE; return; } if (( matches = match( &request_rx, line, item->count, 1 )) == NULL ) { if ( matched ) { remove_conn( item ); return; } } else { /* * These may not be NULL if we are reading a (possibly * maliciously) screwed-up request header with multiple request * lines. We prevent memory leaks by freeing the current strings * before assigning the new. */ if ( item->method != NULL ) free( item->method ); item->method = matches[ 0 ]; if ( item->request != NULL ) free( item->request ); item->request = matches[ 1 ]; if ( item->version != NULL ) free( item->version ); item->version = matches[ 2 ]; continue; } if ( match( &close_rx, line, item->count, 0 ) != NULL ) { item->close = 1; continue; } if ( match( &expect_rx, line, item->count, 0 ) != NULL ) { item->expect = 1; continue; } if (( matches = match( &host_rx, line, item->count, 1 )) == NULL ) { if ( matched ) { remove_conn( item ); return; } } else { /* * Prevent memory leak if there are multiple Host headers. */ if ( item->host != NULL ) free( item->host ); item->host = matches[ 0 ]; continue; } if (( matches = match( &agent_rx, line, item->count, 1 )) == NULL ) { if ( matched ) { remove_conn( item ); return; } } else { /* * Prevent memory leak. */ if ( item->agent != NULL ) free( item->agent ); item->agent = matches[ 0 ]; continue; } if (( matches = match( &length_rx, line, item->count, 1 )) == NULL ) { if ( matched ) { remove_conn( item ); return; } } else { /* * Prevent memory leak. */ if ( item->length != NULL ) free( item->length ); item->length = matches[ 0 ]; continue; } if (( matches = match( &since_rx, line, item->count, 1 )) == NULL ) { if ( matched ) { remove_conn( item ); return; } } else { if ( item->since != NULL ) free( item->since ); item->since = matches[ 0 ]; } if (( matches = match( &cookie_rx, line, item->count, 1 )) == NULL ) { if ( matched ) { remove_conn( item ); return; } } else { /* * Prevent memory leak. */ if ( item->cookie != NULL ) free( item->cookie ); item->cookie = matches[ 0 ]; } } } void add_common_headers( struct ccb *item, int need_date ) { char buffer[ 64 ]; int len; strcat( item->buffer, "Server: Drood/" VERSION ); if ( ! uts ) { strcat( item->buffer, " (" ); strcat( item->buffer, un.sysname ); strcat( item->buffer, "/" ); strcat( item->buffer, un.release ); strcat( item->buffer, "/" ); strcat( item->buffer, un.machine ); strcat( item->buffer, ")" ); } strcat( item->buffer, "\r\n" ); if ( need_date ) { time_t t; struct tm *lt; t = time( NULL ); lt = gmtime( &t ); strftime( buffer, sizeof( buffer ), "Date: %a, %d %b %Y %H:%M:%S %Z\r\n", lt ); len = strlen( buffer ) - 2; buffer[ --len ] = 'T'; buffer[ --len ] = 'M'; buffer[ --len ] = 'G'; strcat( item->buffer, buffer ); } } int fork_cgi( char *prog, struct ccb *item ) { int pfd[ 2 ], flags, pid, ourend; char *ptr, *ptr2, *args[ 2 ]; ourend = -1; if ( pipe( &pfd[ 0 ] ) < 0 ) { syslog( LOG_ERR, "pipe(): %m" ); return ourend; } switch(( pid = fork() )) { case -1: close( pfd[ 0 ] ); close( pfd[ 1 ] ); syslog( LOG_ERR, "fork(): %m" ); return ourend; case 0: fclose( stdin ); fclose( stdout ); if (( dup2( pfd[ 1 ], 1 )) < 0 ) { syslog( LOG_ERR, "dup2: %m" ); _exit( 1 ); } close( pfd[ 0 ] ); close( pfd[ 1 ] ); if (( dup2( item->sock, 0 )) < 0 ) { syslog( LOG_ERR, "dup2: %m" ); _exit( 1 ); } close( item->sock ); if (( flags = fcntl( 0, F_GETFL, 0 )) < 0 ) { syslog( LOG_ERR, "fcntl(): %m" ); _exit( 1 ); } if ( fcntl( 0, F_SETFL, flags & ~O_NONBLOCK ) < 0 ) { syslog( LOG_ERR, "fcntl(): %m" ); _exit( 1 ); } if (( stdout = fdopen( 1, "w" )) == NULL ) { syslog( LOG_ERR, "fdopen(): %m" ); _exit( 1 ); } if (( stdin = fdopen( 0, "r" )) == NULL ) { syslog( LOG_ERR, "fdopen(): %m" ); _exit( 1 ); } while(( ptr = stack_pop( item->working )) != NULL ) { ptr2 = stack_pop( item->working ); setenv( ptr2, ptr, 1 ); free( ptr ); free( ptr2 ); } args[ 0 ] = prog; args[ 1 ] = NULL; execv( args[ 0 ], args ); syslog( LOG_ERR, "execv(): %m" ); _exit( 1 ); default: close( pfd[ 1 ] ); ourend = pfd[ 0 ]; non_blocking( ourend ); } return ourend; } int add_to_env( struct ccb *item, char *str ) { char *ptr; if (( ptr = str_dup( str, strlen( str ))) == NULL ) return 1; if ( stack_push( item->working, ptr ) == NULL ) { free( ptr ); return 1; } return 0; } void prepare_dynamic( struct ccb *item ) { char *ptr, *prog, *args; struct stat st; static struct string *s = NULL; char length[ 32 ]; if ( item->expect ) { *item->buffer = '\0'; strcpy( item->buffer, item->version ); strcat( item->buffer, " 100 Continue\r\n\r\n" ); item->expect = 0; item->next = CONTINUE; item->count = strlen( item->buffer ); item->written = 0; item->state = HEADER; return; } /* * We push all the environment variable data onto the working stack * so we can install them in the child process, avoiding the memory * leak described in setenv(3) in the parent. This drove me crazy for * two days. */ if ( item->working == NULL && ( item->working = make_stack()) == NULL ) { remove_conn( item ); return; } if ( add_to_env( item, "REMOTE_ADDR" ) || add_to_env( item, item->addr )) { remove_conn( item ); return; } if ( s == NULL && ( s = make_string()) == NULL ) { remove_conn( item ); return; } s->free += s->used; s->used = 0; s->top = s->str; for( ptr = item->method; *ptr; ++ptr ) if ( string_append( s, toupper( *ptr )) == NULL ) { remove_conn( item ); return; } if ( add_to_env( item, "REQUEST_METHOD" ) || add_to_env( item, s->str )) { remove_conn( item ); return; } s->free += s->used; s->used = 0; s->top = s->str; if ( string_append( s, '.' ) == NULL ) { remove_conn( item ); return; } if ( string_append( s, '/' ) == NULL ) { remove_conn( item ); return; } for( ptr = item->request; *ptr; ++ptr ) if ( *ptr == '?' ) break; else if ( string_append( s, *ptr ) == NULL ) { remove_conn( item ); return; } prog = s->str; args = ( *ptr == '?' ? ++ptr : "" ); if ( stat( prog, &st ) < 0 ) { if ( errno == ENOENT || errno == ENOTDIR ) { item->count = 0; item->written = 0; item->state = ERROR; item->error = ERR_NOTFOUND; return; } if ( errno == EACCES ) { item->count = 0; item->written = 0; item->state = ERROR; item->error = ERR_FORBIDDEN; return; } item->count = 0; item->written = 0; item->state = ERROR; item->error = ERR_INTERNAL; return; } /* * Check for file type and permissions. */ if ( ! S_ISREG( st.st_mode ) || ( ! (( S_IRUSR & st.st_mode ) && passwd->pw_uid == st.st_uid ) && ! (( S_IRGRP & st.st_mode ) && group->gr_gid == st.st_gid ) && ! ( S_IROTH & st.st_mode ) ) || ( ! (( S_IXUSR & st.st_mode ) && passwd->pw_uid == st.st_uid ) && ! (( S_IXGRP & st.st_mode ) && group->gr_gid == st.st_gid ) && ! ( S_IXOTH & st.st_mode ) )) { item->count = 0; item->written = 0; item->state = ERROR; item->error = ERR_FORBIDDEN; return; } /* * We refuse to run the CGI program just to generate a header. * This response merely indicates the CGI resource exists and is * runnable. */ if ( ! item->type ) { item->buffer[ 0 ] = '\0'; strcpy( item->buffer, item->version ); strcat( item->buffer, " 200 OK\r\n" ); add_common_headers( item, 1 ); strcat( item->buffer, "\r\n" ); item->written = 0; item->count = strlen( item->buffer ); item->state = HEADER; return; } if ( add_to_env( item, "SCRIPT_NAME" ) || add_to_env( item, prog ) || add_to_env( item, "QUERY_STRING" ) || add_to_env( item, args ) || add_to_env( item, "SERVER_SOFTWARE" ) || add_to_env( item, "Drood/" VERSION ) || add_to_env( item, "GATEWAY_INTERFACE" ) || add_to_env( item, "CGI/1.1" ) || add_to_env( item, "HTTP_USER_AGENT" ) || add_to_env( item, ( item->agent == NULL ? "" : item->agent )) || add_to_env( item, "HTTP_COOKIE" ) || add_to_env( item, ( item->cookie == NULL ? "" : item->cookie )) || add_to_env( item, "SERVER_NAME" ) || add_to_env( item, item->host )) { remove_conn( item ); return; } if ( item->type == 1 ) { snprintf( length, sizeof( length ), "%u", ( unsigned int)strlen( args )); if ( add_to_env( item, "CONTENT_LENGTH" ) || add_to_env( item, length )) { remove_conn( item ); return; } } else { if ( item->length == NULL ) { item->count = 0; item->written = 0; item->state = ERROR; item->error = ERR_LENGTHREQ; return; } if ( add_to_env( item, "CONTENT_LENGTH" ) || add_to_env( item, item->length )) { remove_conn( item ); return; } } /* * Will be re-used in filter_cgi_header(). */ if ( item->length != NULL ) { free( item->length ); item->length = NULL; } *item->buffer = '\0'; item->count = 0; item->written = 0; if (( item->source = fork_cgi( prog, item )) < 0 ) { item->state = ERROR; item->error = ERR_INTERNAL; } else { item->state = FILTER; ev_set( item->source, EVFILT_READ, EV_ADD, item ); ev_set( item->source, EVFILT_WRITE, EV_DISABLE, item ); } while(( ptr = stack_pop( item->working )) != NULL ) free( ptr ); return; } void prepare_static( struct ccb *item ) { static struct string *s = NULL; int len; struct stat st; struct tm *lt; char *ptr, **matches, type[ 128 ], size[ 128 ], modified[ 128 ]; if ( item->type == 2 ) { item->count = 0; item->error = ERR_BADREQUEST; item->state = ERROR; return; } if ( s == NULL && ( s = make_string()) == NULL ) { remove_conn( item ); return; } s->free += s->used; s->used = 0; s->top = s->str; /* * We make sure there is at least one separator between the various * items of the path we are going to create in s, because the * command-line arguments may not have included them, and the resource * specified in the request may have not begun with a slash. Multiple * separators are tolerated by the filesystem-related system calls. */ if ( string_append( s, '.' ) == NULL ) { remove_conn( item ); return; } if ( string_append( s, '/' ) == NULL ) { remove_conn( item ); return; } for ( ptr = item->host; *ptr; ++ptr ) if ( string_append( s, *ptr ) == NULL ) { remove_conn( item ); return; } if ( string_append( s, '/' ) == NULL ) { remove_conn( item ); return; } for( ptr = item->request; *ptr; ++ptr ) if ( string_append( s, *ptr ) == NULL ) { remove_conn( item ); return; } if ( stat( s->str, &st ) < 0 ) { if ( errno == ENOENT || errno == ENOTDIR ) { item->count = 0; item->written = 0; item->state = ERROR; item->error = ERR_NOTFOUND; return; } if ( errno == EACCES ) { item->count = 0; item->written = 0; item->state = ERROR; item->error = ERR_FORBIDDEN; return; } item->count = 0; item->written = 0; item->state = ERROR; item->error = ERR_INTERNAL; return; } if ( ! S_ISREG( st.st_mode )) { item->count = 0; item->written = 0; item->state = ERROR; item->error = ERR_FORBIDDEN; return; } /* * Don't send any file which is executable. */ if (( S_IXUSR & st.st_mode ) ||( S_IXGRP & st.st_mode ) || S_IXOTH & st.st_mode ) { item->count = 0; item->written = 0; item->state = ERROR; item->error = ERR_FORBIDDEN; return; } if ( ! st.st_size ) { item->count = 0; item->written = 0; item->state = ERROR; item->error = ERR_NOCONTENT; return; } if (( item->source = open( s->str, O_RDONLY )) < 0 ) { if ( errno == EACCES ) { item->count = 0; item->written = 0; item->state = ERROR; item->error = ERR_FORBIDDEN; return; } item->count = 0; item->written = 0; item->state = ERROR; item->error = ERR_INTERNAL; syslog( LOG_ERR, "open(): %m" ); return; } ptr = NULL; if (( matches = match( &suffix_rx, s->str, s->used, 1 )) == NULL ) { if ( matched ) { remove_conn( item ); return; } } else { char **tp; for( tp = type_table; **tp; tp += 2 ) if ( ! strcmp( *tp, *matches )) { ptr = *( tp + 1 ); break; } free( *matches ); } if ( ptr == NULL ) ptr = default_type; lt = gmtime( &st.st_ctime ); strftime( modified, sizeof( modified ), "Last-Modified: %a, %d %b %Y %H:%M:%S %Z", lt ); len = strlen( modified ); modified[ --len ] = 'T'; modified[ --len ] = 'M'; modified[ --len ] = 'G'; if ( item->since != NULL && ! strcmp( item->since, &modified[ 15 ] )) { item->state = ERROR; item->error = ERR_NOTMODIFIED; item->count = 0; item->written = 0; return; } strcat( modified, "\r\n" ); snprintf( type, sizeof( type ), "Content-Type: %s\r\n", ptr ); snprintf( size, sizeof( size ), "Content-Length: %llu\r\n", ( item->type ? ( long long unsigned int )st.st_size : ( long long unsigned int )0 )); *item->buffer = '\0'; strcpy( item->buffer, item->version ); strcat( item->buffer, " 200 OK\r\n" ); strcat( item->buffer, type ); strcat( item->buffer, size ); strcat( item->buffer, modified ); add_common_headers( item, 1 ); strcat( item->buffer, "\r\n" ); item->count = strlen( item->buffer ); item->written = 0; item->size = st.st_size; item->next = RESPOND; item->state = HEADER; return; } void analyze_request( struct ccb *item ) { struct sockaddr_in addr; int i = 0, g = 1, h = 1, p = 1; socklen_t len; char address[ 16 ], buffer[ 2048 ]; len = sizeof( struct sockaddr_in ); if ( getpeername( item->sock, ( struct sockaddr *)&addr, &len ) < 0 ) strcpy( address, "unknown" ); else if ( inet_ntop( AF_INET, &addr.sin_addr, address, sizeof( address )) == NULL ) strcpy( address, "unknown" ); if (( item->addr = str_dup( address, strlen( address ))) == NULL ) { remove_conn( item ); return; } if ( ! nolog ) { snprintf( buffer, sizeof( buffer ), "[%s] {%s} (%s) ", address, ( item->agent == NULL ? "not set" : item->agent ), item->method, ( item->host == NULL ? "default.host" : item->host ), ( *item->request == '/' ? &item->request[ 1 ] : item->request )); syslog( LOG_INFO, "%s", buffer ); } if ( ( item->verno = strcasecmp( item->version, "HTTP/1.0" )) && strcasecmp( item->version, "HTTP/1.1" )) { item->error = ERR_BADVERSION; item->close = 1; item->count = 0; item->written = 0; item->state = ERROR; return; } if ( ! item->verno ) item->close = 1; if ( item->host == NULL ) { if (( item->host = str_dup( "default", 7 )) == NULL ) { remove_conn( item ); return; } } else if ( *item->host == '.' || match( &hostcheck_rx, item->host, strlen( item->host ), 0 ) != NULL ) { item->count = 0; item->written = 0; item->close = 1; item->error = ERR_BADREQUEST; item->state = ERROR; return; } else if ( strcmp( item->host, "127.0.0.1" ) ) for( i = 0; i < hosts->used; ++i ) if ( ! strcmp( ( char *)hosts->values[ i ], item->host )) break; if ( i == hosts->used ) { item->count = 0; item->written = 0; item->close = 1; item->error = ERR_BADREQUEST; item->state = ERROR; return ; } if ( ( g = strcasecmp( item->method, "GET" )) && ( h = strcasecmp( item->method, "HEAD" )) && ( p = strcasecmp( item->method, "POST" ))) { item->error = ERR_BADMETHOD; item->count = 0; item->written = 0; item->state = ERROR; return; } if ( !h ) item->type = 0; else if ( !g ) item->type = 1; else if ( !p ) item->type = 2; if ( !( *item->request )) { item->error = ERR_BADREQUEST; item->count = 0; item->written = 0; item->close = 1; item->state = ERROR; return; } len = strlen( item->request ); if ( match( &parent_rx, item->request, len, 0 ) != NULL ) { item->error = ERR_BADREQUEST; item->count = 0; item->written = 0; item->state = ERROR; item->close = 1; return; } if ( ! strcmp( item->request, "/" )) { free( item->request ); item->request = NULL; len = strlen( root_resource ); if (( item->request = str_dup( root_resource, len )) == NULL ) { remove_conn( item ); return; } } if ( dynamic != NULL && match( &dynamic_rx, item->request, len, 0 ) != NULL ) prepare_dynamic( item ); else prepare_static( item ); return; } void respond_header( struct ccb *item ) { off_t written; written = item->written; item->written = write( item->sock, &item->buffer[ item->written ], item->count ); if ( item->written < 0 ) { remove_conn( item ); return; } item->count -= item->written; if ( ! item->count ) { item->written = 0; if ( ! item->type || (( item->state = item->next ) == HEADER )) { if ( item->close ) remove_conn( item ); else clear_conn( item ); } else { if ( item->state == CHUNK || item->state == NOCHUNK ) { ev_set( item->source, EVFILT_READ, EV_ENABLE, item ); ev_set( item->sock, EVFILT_WRITE, EV_DISABLE, item ); } } } return; } /* * respond_error() is tiggered by the writeability of a client socket for a * connection in the ERROR state. Because we know the socket is writable, * we know write() will not generate EWOULDBLOCK, but it can return short * counts. If write() returns an error, we drop the connection, and do not * bother to test for EWOULDBLOCK. */ void respond_error( struct ccb *item ) { off_t written; if ( ! item->count ) { char buffer[ 512 ], len_buffer[ 64 ]; *item->buffer = '\0'; if ( item->version == NULL ) { strcpy( item->buffer, "HTTP/1.0" ); item->close = 1; } else strcpy( item->buffer, item->version ); strcat( item->buffer, errstrings[ item->error ] ); add_common_headers( item, 1 ); strcpy( buffer, "" ); strcat( buffer, errstrings[ item->error ] ); strcat( buffer, "" ); strcat( buffer, errstrings[ item->error ] ); strcat( buffer, "" ); snprintf( len_buffer, sizeof( len_buffer ), "Content-Length: %u\r\n", ( unsigned int )strlen( buffer )); strcat( item->buffer, len_buffer ); strcat( item->buffer, "\r\n" ); strcat( item->buffer, buffer ); item->count = strlen( item->buffer ); } written = item->written; item->written = write( item->sock, &item->buffer[ item->written ], item->count ); if ( item->written < 0 ) { remove_conn( item ); return; } item->count -= item->written; if ( ! item->count ) { if ( item->close ) remove_conn( item ); else clear_conn( item ); return; } return; } void respond_static( struct ccb *item ) { off_t written = 0; if ( sendfile( item->source, item->sock, item->written, MIN( item->size, IOBUFSIZE ), NULL, &written, 0 ) < 0 ) { if ( errno != EWOULDBLOCK && errno != EAGAIN ) { remove_conn( item ); return; } } item->written += written; item->size -= written; if ( ! item->size ) { if ( item->close ) remove_conn( item ); else clear_conn( item ); } return; } /* * Both respond_chunked() and respond_unchunked() are triggered by the * readability of CGI pipes for clients in the CHUNK and NOCHUNK states, * respectively. Because the pipe is readable, read() will not generate * EWOULDBLOCK, but it can return short counts. If it returns an error, we * drop the connection, rather than test for EWOULDBLOCK. */ void respond_unchunked( struct ccb *item ) { int result; off_t written; if ( ! item->count ) { switch(( result = read( item->source, item->buffer, IOBUFSIZE ) )) { case 0: if ( item->close ) remove_conn( item ); else clear_conn( item ); return; case -1: remove_conn( item ); return; default: item->count = result; item->written = 0; } } written = item->written; item->written = write( item->sock, &item->buffer[ item->written ], item->count ); if ( item->written < 0 ) { if ( errno != EWOULDBLOCK && errno != EAGAIN ) remove_conn( item ); else item->written = written; return; } item->count -= item->written; return; } void respond_chunked( struct ccb *item ) { int result = 1, written; if ( ! item->count ) { if (( result = read( item->source, &item->buffer[ 6 ], IOBUFSIZE - 8 )) < 0 ) { if ( errno != EWOULDBLOCK && errno != EAGAIN ) remove_conn( item ); return; } item->buffer[ result + 6 ] = '\r'; item->buffer[ result + 7 ] = '\n'; item->count = result + 8; item->written = 0; snprintf( item->buffer, 6, "%.4X\r\n", result ); item->buffer[ 5 ] = '\n'; } written = item->written; item->written = write( item->sock, &item->buffer[ item->written ], item->count ); if ( item->written < 0 ) { if ( errno != EWOULDBLOCK && errno != EAGAIN ) remove_conn( item ); else item->written = written; return; } item->count -= item->written; if ( ! item->count && ! result ) { if ( item->close ) remove_conn( item ); else clear_conn( item ); } return; } void filter_cgi_header( struct ccb *item ) { char *line, *ptr, **matches; int status = 0, date = 1, type = 0, len = 0; for( ; ; ) { if (( line = get_header_line( item, 0 )) == NULL ) { if ( notready == 1 ) return; else { /* * 0 means premature EOF from CGI program or SIGTERM. * 2 means the CGI program sent a too-long line. * We bail in each case. */ item->count = 0; item->close = 1; item->written = 0; item->error = ERR_INTERNAL; item->state = ERROR; flip_events( item ); break; } } /* * If we have the request header separator line, we are done here. */ if ( *line == '\0' ) break; ptr = str_dup( line, item->count ); /* * CGI programs must not create headers larger than the ccb->buffer * - 1000. This give us space to supplement the header without * the possibility of buffer overrun. */ if (( len += strlen( ptr ) > IOBUFSIZE - 1000 )) { free( ptr ); remove_conn( item ); return; } if ( stack_push( item->working, ptr ) == NULL ) { remove_conn( item ); return; } if ( match( &type_rx, line, item->count, 0 ) != NULL ) { type = 1; continue; } if ( match( &location_rx, line, item->count, 0 ) != NULL ) { status = 1; continue; } if ( match( &date_rx, line, item->count, 0 ) != NULL ) { date = 0; continue; } if (( matches = match( &length_rx, line, item->count, 1 )) == NULL ) { if ( matched ) { remove_conn( item ); return; } } else { if ( item->length != NULL ) free( item->length ); item->length = matches[ 0 ]; } } item->written = 0; *item->buffer = '\0'; if ( status ) item->next = HEADER; else if ( ! item->verno || item->length != NULL ) item->next = NOCHUNK; else item->next = CHUNK; item->state = HEADER; strcpy( item->buffer, item->version ); strcat( item->buffer, ( status ? " 302 Found\r\n" : " 200 OK\r\n" )); add_common_headers( item, date ); if ( status ) strcat( item->buffer, "Content-Length: 0\r\n" ); else if ( ! type ) { strcat( item->buffer, "Content-Type: " ); strcat( item->buffer, default_type ); strcat( item->buffer, "\r\n" ); } if ( ! status && item->next == CHUNK ) strcat( item->buffer, "Transfer-Encoding: chunked\r\n" ); if ( item->close ) strcat( item->buffer, "Connection: close\r\n" ); while(( line = ( char *)stack_pop( item->working )) != NULL ) { strcat( item->buffer, line ); strcat( item->buffer, "\r\n" ); free( line ); } strcat( item->buffer, "\r\n" ); item->count = strlen( item->buffer ); non_blocking( item->sock ); flip_events( item ); return; } /* * If a timer fires on a descriptor and that descriptor becomes readable * after the last invocation of kevent(), we could get the both events in * the queue, on the next invocation. If the read event was earlier in the * queue, and in processing the read event, it was decided to close the * connection, its ccb would be freed with a call to remove_conn(). When * the timer event was encountered a second freeing of the ccb would occur. * If the events were placed in the queue in the opposite order, then * processing the timer event would call remove_conn(), and then the read * event handler would be dispatched with a pointer to the freed ccb. * Our solution is to modify the output queue every time a connection is * dropped during the processing of a read, write, or timer event. We make * sure any other events for the same descriptor further along in the queue * are ignored by setting the ident member of the appropriate struct kevent * to -1. * kevent() returns -1 when there is no room to create an error event in * the output queue. We treat this case the same as an error event in the * output queue, and just move on. The errors generated are from duplicate * delete timer events in the input queue, and the occasional failure to * create a read or a write event on a connection unexpectedly dropped by * the client. When kevent() does return -1, the output queue may contain * any other sort of event. If it is an error, we don't care. If it is an * IO event, they are level-triggered, so we will get those events again. * If is is a timer event, it will be generated again, for timers are * periodic. No event is lost. The worst that can happen is that a * time-out occurs later than it normally would. */ void process_clients() { int kq, conn, out, n; struct ccb *item; struct kevent outqueue[ QLEN ]; struct sockaddr_in client_addr; socklen_t client_len; client_len = sizeof( client_addr ); if (( kq = kqueue()) < 0 ) { syslog( LOG_ERR, "kqueue(): %m" ); exit( 1 ); } non_blocking( fd ); ev_set( fd, EVFILT_READ, EV_ADD | EV_ENABLE, NULL ); for( ; ; ) { out = kevent( kq, inqueue, in, outqueue, QLEN, NULL ); in = 0; if ( sigterm ) break; if ( out <= 0 ) continue; for( n = 0; n < out; ++n ) { if ( outqueue[ n ].flags & EV_ERROR ) continue; if ( outqueue[ n ].ident < 0 ) continue; if ( outqueue[ n ].filter == EVFILT_TIMER ) { int c; for( c = n + 1; c < out; ++c ) if ( outqueue[ n ].ident == outqueue[ c ].ident ) outqueue[ c ].ident = -1; remove_conn( outqueue[ n ].udata ); continue; } if ( outqueue[ n ].ident == fd ) { conn = accept( fd, ( struct sockaddr *)&client_addr, &client_len ); if ( conn < 0 ) { if ( errno != EWOULDBLOCK && errno != EAGAIN ) syslog( LOG_ERR, "accept(): %m" ); } else { non_blocking( conn ); add_conn( conn ); } continue; } if ( outqueue[ n ].ident >= 0 ) { closed = 0; item = ( struct ccb *)outqueue[ n ].udata; handlers[ item->state ]( item ); if ( closed ) { int c; for( c = n + 1; c < out; ++c ) if ( outqueue[ c ].ident == closed ) outqueue[ c ].ident = -1; } } } } } int main( int argc, char **argv ) { init_var( argc, argv ); init_rx(); if ( !testing ) become_daemon(); start_listening(); process_clients(); return 0; }