#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "httpd.h" /* ---------------------------------------------------------------------- */ /* public variables - server configuration */ char *server_name = "webfs/" WEBFS_VERSION; int debug = 0; int dontdetach = 0; int timeout = 60; int keepalive_time = 5; int tcp_port = 0; int max_dircache = 128; char *doc_root = "."; char *indexhtml = NULL; char *cgipath = NULL; char *listen_ip = NULL; char *listen_port = "8000"; int virtualhosts = 0; int canonicalhost = 0; char server_host[256]; char user[17]; char group[17]; char *mimetypes = MIMEFILE; char *pidfile = NULL; char *logfile = NULL; FILE *logfh = NULL; char *userpass = NULL; char *userdir = NULL; int flushlog = 0; int do_chroot = 0; int usesyslog = 0; int have_tty = 1; int max_conn = 32; int lifespan = -1; int no_listing = 0; time_t now; int slisten; #ifdef USE_THREADS pthread_mutex_t lock_logfile = PTHREAD_MUTEX_INITIALIZER; int nthreads = 1; pthread_t *threads; #endif #ifdef USE_SSL char *certificate = "server.pem"; char *password; int with_ssl = 0; SSL_CTX *ctx; BIO *sbio, *ssl_bio; #endif /* ---------------------------------------------------------------------- */ static int termsig,got_sighup; static void catchsig(int sig) { if (SIGTERM == sig || SIGINT == sig) termsig = sig; if (SIGHUP == sig) got_sighup = 1; } /* ---------------------------------------------------------------------- */ static void usage(char *name) { char *h; struct passwd *pw; struct group *gr; h = strrchr(name,'/'); fprintf(stderr, "This is a lightweight http server for static content\n" "\n" "usage: %s [ options ]\n" "\n" "Options:\n" " -h print this text\n" " -4 use ipv4\n" " -6 use ipv6\n" " -d enable debug output [%s]\n" " -F do not fork into background [%s]\n" " -s enable syslog (start/stop/errors) [%s]\n" " -t sec set network timeout [%i]\n" " -c n set max. allowed connections [%i]\n" " -a n set max. cached dirs [%i]\n" " -j disable directory listings [%s]\n" #ifdef USE_THREADS " -y n startup n threads [%i]\n" #endif " -p port use tcp-port >port< [%s]\n" " -r dir document root is >dir< [%s]\n" " -R dir same as above + chroot to >dir<\n" " -f file look for >file< as directory index [%s]\n" " -n host server hostname is >host< [%s]\n" " -N host same as above + UseCanonicalName\n" " -i ip bind to IP-address >ip< [%s]\n" " -v enable virtual hosts [%s]\n" " -l log write access log to file >log< [%s]\n" " -L log same as above + flush every line\n" " -m file read mime types from >file< [%s]\n" " -k file use >file< as pidfile [%s]\n" " -b user:pass password protect the exported\n" " files (basic authentication)\n" " -e sec limit live span of files to sec\n" " seconds (using expires header)\n" #ifdef USE_SSL " -S enable SSL mode\n" " -C file SSL-Certificate file [%s]\n" " -P pass SSL-Certificate password\n" #endif " -x dir CGI script directory (relative to\n" " document root) [%s]\n" " -~ dir user home directory (will expand\n" " /~user/path to $HOME/dir/path\n", h ? h+1 : name, debug ? "on" : "off", dontdetach ? "on" : "off", usesyslog ? "on" : "off", timeout, max_conn, max_dircache, no_listing ? "on" : "off", #ifdef USE_THREADS nthreads, #endif listen_port, doc_root, indexhtml ? indexhtml : "none", server_host, listen_ip ? listen_ip : "any", virtualhosts ? "on" : "off", logfile ? logfile : "none", mimetypes, pidfile ? pidfile : "none", #ifdef USE_SSL certificate, #endif cgipath ? cgipath : "none"); if (getuid() == 0) { pw = getpwuid(0); gr = getgrgid(getgid()); fprintf(stderr, " -u user run as user >user< [%s]\n" " -g group run as group >group< [%s]\n", pw ? pw->pw_name : "???", gr ? gr->gr_name : "???"); } exit(1); } static void run_as(int id) { if (-1 == seteuid(id)) { fprintf(stderr,"seteuid(%d): %s\n",id,strerror(errno)); exit(1); } if (debug) fprintf(stderr,"run_as: uid=%d euid=%d\n",getuid(),geteuid()); } static void fix_ug(void) { struct passwd *pw = NULL; struct group *gr = NULL; /* root is allowed to use any uid/gid, * others will get their real uid/gid */ if (0 == getuid() && strlen(user) > 0) { if (NULL == (pw = getpwnam(user))) pw = getpwuid(atoi(user)); } else { pw = getpwuid(getuid()); } if (0 == getuid() && strlen(group) > 0) { if (NULL == (gr = getgrnam(group))) gr = getgrgid(atoi(group)); } else { gr = getgrgid(getgid()); } if (NULL == pw) { xerror(LOG_ERR,"user unknown",NULL); exit(1); } if (NULL == gr) { xerror(LOG_ERR,"group unknown",NULL); exit(1); } /* chroot to $DOCUMENT_ROOT (must be done here as getpwuid needs /etc and chroot works as root only) */ if (do_chroot) { chdir(doc_root); if (-1 == chroot(doc_root)) { xperror(LOG_ERR,"chroot",NULL); exit(1); } } /* set group */ if (getegid() != gr->gr_gid || getgid() != gr->gr_gid) { setgid(gr->gr_gid); setgroups(0, NULL); } if (getegid() != gr->gr_gid || getgid() != gr->gr_gid) { xerror(LOG_ERR,"setgid failed",NULL); exit(1); } strncpy(group,gr->gr_name,16); /* set user */ if (geteuid() != pw->pw_uid || getuid() != pw->pw_uid) setuid(pw->pw_uid); if (geteuid() != pw->pw_uid || getuid() != pw->pw_uid) { xerror(LOG_ERR,"setuid failed",NULL); exit(1); } strncpy(user,pw->pw_name,16); if (debug) fprintf(stderr,"fix_ug: uid=%d euid=%d / gid=%d egid=%d\n", getuid(),geteuid(),getgid(),getegid()); } /* ---------------------------------------------------------------------- */ static void access_log(struct REQUEST *req, time_t now) { char timestamp[32]; DO_LOCK(lock_logfile); if (NULL == logfh) { DO_UNLOCK(lock_logfile); return; } /* common log format: host ident authuser date request status bytes */ strftime(timestamp,31,"[%d/%b/%Y:%H:%M:%S +0000]",localtime(&now)); if (0 == req->status) req->status = 400; /* bad request */ if (400 == req->status) { fprintf(logfh,"%s - - %s \"-\" 400 %d\n", req->peerhost, timestamp, req->bc); } else { fprintf(logfh,"%s - - %s \"%s %s HTTP/%d.%d\" %d %d\n", req->peerhost, timestamp, req->type, req->uri, req->major, req->minor, req->status, req->bc); } if (flushlog) fflush(logfh); DO_UNLOCK(lock_logfile); } /* * loglevel usage * ERR : fatal errors (which are followed by exit(1)) * WARNING: this should'nt happen error (oom, ...) * NOTICE : start/stop of the daemon * INFO : "normal" errors (canceled downloads, timeouts, * stuff what happens all the time) */ static void syslog_init(void) { openlog("webfsd",LOG_PID, LOG_DAEMON); } static void syslog_start(void) { syslog(LOG_NOTICE, "started (listen on %s:%d, root=%s, user=%s, group=%s)\n", listen_ip ? listen_ip : "*", tcp_port,doc_root,user,group); } static void syslog_stop(void) { if (termsig) syslog(LOG_NOTICE,"stopped on signal %d (%s)\n", termsig,strsignal(termsig)); else syslog(LOG_NOTICE,"stopped\n"); closelog(); } void xperror(int loglevel, char *txt, char *peerhost) { if (LOG_INFO == loglevel && usesyslog < 2 && !debug) return; if (have_tty) { if (NULL == peerhost) perror(txt); else fprintf(stderr,"%s: %s (peer=%s)\n",txt,strerror(errno), peerhost); } if (usesyslog) { if (NULL == peerhost) syslog(loglevel,"%s: %s\n",txt,strerror(errno)); else syslog(loglevel,"%s: %s (peer=%s)\n",txt,strerror(errno), peerhost); } } void xerror(int loglevel, char *txt, char *peerhost) { if (LOG_INFO == loglevel && usesyslog < 2 && !debug) return; if (have_tty) { if (NULL == peerhost) fprintf(stderr,"%s\n",txt); else fprintf(stderr,"%s (peer=%s)\n",txt,peerhost); } if (usesyslog) { if (NULL == peerhost) syslog(loglevel,"%s\n",txt); else syslog(loglevel,"%s (peer=%s)\n",txt,peerhost); } } /* ---------------------------------------------------------------------- */ /* main loop */ static void* mainloop(void *thread_arg) { struct REQUEST *conns = NULL; int curr_conn = 0; struct REQUEST *req,*prev,*tmp; struct timeval tv; int max,length; fd_set rd,wr; for (;!termsig;) { if (got_sighup) { if (NULL != logfile && 0 != strcmp(logfile,"-")) { if (debug) fprintf(stderr,"got SIGHUP, reopen logfile %s\n",logfile); DO_LOCK(lock_logfile); if (logfh) fclose(logfh); if (NULL == (logfh = fopen(logfile,"a"))) xperror(LOG_WARNING,"reopen access log",NULL); else close_on_exec(fileno(logfh)); DO_UNLOCK(lock_logfile); } got_sighup = 0; } FD_ZERO(&rd); FD_ZERO(&wr); max = 0; /* add listening socket */ if (curr_conn < max_conn) { FD_SET(slisten,&rd); max = slisten; } /* add connection sockets */ for (req = conns; req != NULL; req = req->next) { switch (req->state) { case STATE_KEEPALIVE: case STATE_READ_HEADER: FD_SET(req->fd,&rd); if (req->fd > max) max = req->fd; break; case STATE_WRITE_HEADER: case STATE_WRITE_BODY: case STATE_WRITE_FILE: case STATE_WRITE_RANGES: case STATE_CGI_BODY_OUT: FD_SET(req->fd,&wr); #ifdef USE_SSL if (with_ssl) FD_SET(req->fd,&rd); #endif if (req->fd > max) max = req->fd; break; case STATE_CGI_HEADER: case STATE_CGI_BODY_IN: FD_SET(req->cgipipe,&rd); if (req->cgipipe > max) max = req->cgipipe; break; } } /* go! */ tv.tv_sec = keepalive_time; tv.tv_usec = 0; if (-1 == select(max+1,&rd,&wr,NULL,(curr_conn > 0) ? &tv : NULL)) { if (debug) perror("select"); continue; } now = time(NULL); /* new connection ? */ if (FD_ISSET(slisten,&rd)) { req = malloc(sizeof(struct REQUEST)); if (NULL == req) { /* oom: let the request sit in the listen queue */ if (debug) fprintf(stderr,"oom\n"); } else { memset(req,0,sizeof(struct REQUEST)); if (-1 == (req->fd = accept(slisten,NULL,NULL))) { if (EAGAIN != errno) xperror(LOG_WARNING,"accept",NULL); free(req); } else { close_on_exec(req->fd); fcntl(req->fd,F_SETFL,O_NONBLOCK); req->bfd = -1; req->cgipipe = -1; req->state = STATE_READ_HEADER; req->ping = now; req->next = conns; conns = req; curr_conn++; if (debug) fprintf(stderr,"%03d: new request (%d)\n",req->fd,curr_conn); #ifdef USE_SSL if (with_ssl) open_ssl_session(req); #endif length = sizeof(req->peer); if (-1 == getpeername(req->fd,(struct sockaddr*)&(req->peer),&length)) { xperror(LOG_WARNING,"getpeername",NULL); req->state = STATE_CLOSE; } getnameinfo((struct sockaddr*)&req->peer,length, req->peerhost,64,req->peerserv,8, NI_NUMERICHOST | NI_NUMERICSERV); if (debug) fprintf(stderr,"%03d: connect from (%s)\n", req->fd,req->peerhost); } } } /* check active connections */ for (req = conns, prev = NULL; req != NULL;) { /* handle I/O */ switch (req->state) { case STATE_KEEPALIVE: case STATE_READ_HEADER: if (FD_ISSET(req->fd,&rd)) { req->state = STATE_READ_HEADER; read_request(req,0); req->ping = now; } break; case STATE_WRITE_HEADER: case STATE_WRITE_BODY: case STATE_WRITE_FILE: case STATE_WRITE_RANGES: case STATE_CGI_BODY_OUT: if (FD_ISSET(req->fd,&wr)) { write_request(req); req->ping = now; } #ifdef USE_SSL if (with_ssl && FD_ISSET(req->fd,&rd)) { write_request(req); req->ping = now; } #endif break; case STATE_CGI_HEADER: if (FD_ISSET(req->cgipipe,&rd)) { cgi_read_header(req); req->ping = now; } break; case STATE_CGI_BODY_IN: if (FD_ISSET(req->cgipipe,&rd)) { write_request(req); req->ping = now; } break; } /* check timeouts */ if (req->state == STATE_KEEPALIVE) { if (now > req->ping + keepalive_time || curr_conn > max_conn * 9 / 10) { if (debug) fprintf(stderr,"%03d: keepalive timeout\n",req->fd); req->state = STATE_CLOSE; } } else { if (now > req->ping + timeout) { if (req->state == STATE_READ_HEADER) { mkerror(req,408,0); } else { xerror(LOG_INFO,"network timeout",req->peerhost); req->state = STATE_CLOSE; } } } /* header parsing */ header_parsing: if (req->state == STATE_PARSE_HEADER) { parse_request(req); if (req->state == STATE_WRITE_HEADER) write_request(req); } /* handle finished requests */ if (req->state == STATE_FINISHED && !req->keep_alive) req->state = STATE_CLOSE; if (req->state == STATE_FINISHED) { if (logfh) access_log(req,now); /* cleanup */ req->auth[0] = 0; req->if_modified = NULL; req->if_unmodified = NULL; req->if_range = NULL; req->range_hdr = NULL; req->ranges = 0; if (req->r_start) { free(req->r_start); req->r_start = NULL; } if (req->r_end) { free(req->r_end); req->r_end = NULL; } if (req->r_head) { free(req->r_head); req->r_head = NULL; } if (req->r_hlen) { free(req->r_hlen); req->r_hlen = NULL; } list_free(&req->header); memset(req->mtime, 0, sizeof(req->mtime)); if (req->bfd != -1) { close(req->bfd); req->bfd = -1; } if (req->cgipipe != -1) { close(req->cgipipe); req->cgipipe = -1; } if (req->cgipid) { kill(req->cgipid,SIGTERM); req->cgipid = 0; } req->body = NULL; req->written = 0; req->head_only = 0; req->rh = 0; req->rb = 0; if (req->dir) { free_dir(req->dir); req->dir = NULL; } req->hostname[0] = 0; req->path[0] = 0; req->query[0] = 0; if (req->hdata == req->lreq) { /* ok, wait for the next one ... */ if (debug) fprintf(stderr,"%03d: keepalive wait\n",req->fd); req->state = STATE_KEEPALIVE; req->hdata = 0; req->lreq = 0; #ifdef TCP_CORK if (1 == req->tcp_cork) { req->tcp_cork = 0; if (debug) fprintf(stderr,"%03d: tcp_cork=%d\n",req->fd,req->tcp_cork); setsockopt(req->fd,SOL_TCP,TCP_CORK,&req->tcp_cork,sizeof(int)); } #endif } else { /* there is a pipelined request in the queue ... */ if (debug) fprintf(stderr,"%03d: keepalive pipeline\n",req->fd); req->state = STATE_READ_HEADER; memmove(req->hreq,req->hreq+req->lreq, req->hdata-req->lreq); req->hdata -= req->lreq; req->lreq = 0; read_request(req,1); goto header_parsing; } } /* connections to close */ if (req->state == STATE_CLOSE) { if (logfh) access_log(req,now); /* cleanup */ close(req->fd); #ifdef USE_SSL if (with_ssl) SSL_free(req->ssl_s); #endif if (req->bfd != -1) close(req->bfd); if (req->cgipipe != -1) close(req->cgipipe); if (req->cgipid) kill(req->cgipid,SIGTERM); if (req->dir) free_dir(req->dir); curr_conn--; if (debug) fprintf(stderr,"%03d: done (%d)\n",req->fd,curr_conn); /* unlink from list */ tmp = req; if (prev == NULL) { conns = req->next; req = conns; } else { prev->next = req->next; req = req->next; } /* free memory */ if (tmp->r_start) free(tmp->r_start); if (tmp->r_end) free(tmp->r_end); if (tmp->r_head) free(tmp->r_head); if (tmp->r_hlen) free(tmp->r_hlen); list_free(&tmp->header); free(tmp); } else { prev = req; req = req->next; } } } return NULL; } /* ---------------------------------------------------------------------- */ int main(int argc, char *argv[]) { struct sigaction act,old; struct addrinfo ask,*res; struct sockaddr_storage ss; int c, opt, rc, ss_len, pid=0, v4 = 1, v6 = 1; int uid,euid; char host[INET6_ADDRSTRLEN+1]; char serv[16]; char mypid[12]; uid = getuid(); euid = geteuid(); if (uid != euid) run_as(uid); gethostname(server_host,255); memset(&ask,0,sizeof(ask)); ask.ai_flags = AI_CANONNAME; if (0 == (rc = getaddrinfo(server_host, NULL, &ask, &res))) { if (res->ai_canonname) strcpy(server_host,res->ai_canonname); } /* parse options */ for (;;) { if (-1 == (c = getopt(argc,argv,"hvsdF46jS" "r:R:f:p:n:N:i:t:c:a:u:g:l:L:m:y:b:k:e:x:C:P:~:"))) break; switch (c) { case 'h': usage(argv[0]); break; case '4': v4 = 1; v6 = 0; break; case '6': v4 = 0; v6 = 1; break; case 's': usesyslog++; break; case 'd': debug++; break; case 'v': virtualhosts++; break; case 'F': dontdetach++; break; case 'R': do_chroot = 1; /* fall through */ case 'r': doc_root = optarg; break; case 'f': indexhtml = optarg; break; case 'N': canonicalhost = 1; /* fall through */ case 'n': strncpy(server_host,optarg,64); break; case 'i': listen_ip = optarg; break; case 'p': listen_port = optarg; break; case 't': timeout = atoi(optarg); break; case 'c': max_conn = atoi(optarg); break; case 'a': max_dircache = atoi(optarg); break; case 'u': strncpy(user,optarg,16); break; case 'g': strncpy(group,optarg,16); break; case 'L': flushlog = 1; /* fall through */ case 'l': logfile = optarg; break; case 'm': mimetypes = optarg; break; case 'k': pidfile = optarg; break; case 'b': userpass = strdup(optarg); memset(optarg,'x',strlen(optarg)); break; case 'e': lifespan = atoi(optarg); break; case 'x': if (optarg[strlen(optarg)-1] == '/') { cgipath = optarg; } else { cgipath = malloc(strlen(optarg)+2); sprintf(cgipath,"%s/",optarg); } break; #ifdef USE_THREADS case 'y': nthreads = atoi(optarg); break; #endif #ifdef USE_SSL case 'S': with_ssl++; break; case 'C': certificate = optarg; break; case 'P': password = strdup(optarg); memset(optarg,'x',strlen(optarg)); break; #endif case 'j': no_listing = 1; break; case '~': userdir = optarg; break; default: exit(1); } } if (usesyslog) syslog_init(); /* bind to socket */ slisten = -1; memset(&ask,0,sizeof(ask)); ask.ai_flags = AI_PASSIVE; if (listen_ip) ask.ai_flags |= AI_CANONNAME; ask.ai_socktype = SOCK_STREAM; /* try ipv6 first ... */ if (-1 == slisten && v6) { ask.ai_family = PF_INET6; if (0 != (rc = getaddrinfo(listen_ip, listen_port, &ask, &res))) { if (debug) fprintf(stderr,"getaddrinfo (ipv6): %s\n",gai_strerror(rc)); } else { if (-1 == (slisten = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) && debug) xperror(LOG_ERR,"socket (ipv6)",NULL); } } /* ... failing that try ipv4 */ if (-1 == slisten && v4) { ask.ai_family = PF_INET; if (0 != (rc = getaddrinfo(listen_ip, listen_port, &ask, &res))) { fprintf(stderr,"getaddrinfo (ipv4): %s\n",gai_strerror(rc)); exit(1); } if (-1 == (slisten = socket(res->ai_family, res->ai_socktype, res->ai_protocol))) { xperror(LOG_ERR,"socket (ipv4)",NULL); exit(1); } } if (-1 == slisten) exit(1); close_on_exec(slisten); memcpy(&ss,res->ai_addr,res->ai_addrlen); ss_len = res->ai_addrlen; if (res->ai_canonname) strcpy(server_host,res->ai_canonname); if (0 != (rc = getnameinfo((struct sockaddr*)&ss,ss_len, host,INET6_ADDRSTRLEN,serv,15, NI_NUMERICHOST | NI_NUMERICSERV))) { fprintf(stderr,"getnameinfo: %s\n",gai_strerror(rc)); exit(1); } tcp_port = atoi(serv); opt = 1; setsockopt(slisten,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); fcntl(slisten,F_SETFL,O_NONBLOCK); /* Use accept filtering, if available. */ #ifdef SO_ACCEPTFILTER { struct accept_filter_arg af; memset(&af,0,sizeof(af)); strcpy(af.af_name,"httpready"); setsockopt(slisten, SOL_SOCKET, SO_ACCEPTFILTER, (char*)&af, sizeof(af)); } #endif /* SO_ACCEPTFILTER */ if (uid != euid) run_as (euid); if (-1 == bind(slisten, (struct sockaddr*) &ss, ss_len)) { xperror(LOG_ERR,"bind",NULL); exit(1); } if (uid != euid) run_as (uid); if (-1 == listen(slisten, 2*max_conn)) { xperror(LOG_ERR,"listen",NULL); exit(1); } /* init misc stuff */ init_mime(mimetypes,"text/plain"); init_quote(); #ifdef USE_SSL if (with_ssl) init_ssl(); #endif /* change user/group - also does chroot */ if (uid != euid) run_as (euid); fix_ug(); if (logfile) { if (0 == strcmp(logfile,"-")) { logfh = stdout; } else { if (NULL == (logfh = fopen(logfile,"a"))) xperror(LOG_WARNING,"open access log",NULL); else close_on_exec(fileno(logfh)); } } if (pidfile) { if (-1 == (pid = open(pidfile,O_WRONLY | O_CREAT | O_EXCL, 0600))) { fprintf(stderr,"open %s: %s\n",pidfile,strerror(errno)); exit(1); } close_on_exec(pid); } if (debug) { fprintf(stderr, "http server started\n" " ipv6 : %s\n" #ifdef USE_SSL " ssl : %s\n" #endif " node : %s\n" " ipaddr: %s\n" " port : %d\n" " export: %s\n" " user : %s\n" " group : %s\n", res->ai_family == PF_INET6 ? "yes" : "no", #ifdef USE_SSL with_ssl ? "yes" : "no", #endif server_host,host,tcp_port,doc_root,user,group); } /* run as daemon - detach from terminal */ if ((!debug) && (!dontdetach)) { switch (fork()) { case -1: xperror(LOG_ERR,"fork",NULL); exit(1); case 0: close(0); close(1); close(2); setsid(); have_tty = 0; break; default: exit(0); } } if (usesyslog) { syslog_start(); atexit(syslog_stop); } if (pidfile) { sprintf(mypid,"%d",getpid()); write(pid,mypid,strlen(mypid)); close(pid); } /* setup signal handler */ memset(&act,0,sizeof(act)); sigemptyset(&act.sa_mask); act.sa_handler = SIG_IGN; sigaction(SIGPIPE,&act,&old); sigaction(SIGCHLD,&act,&old); act.sa_handler = catchsig; sigaction(SIGHUP,&act,&old); sigaction(SIGTERM,&act,&old); if (debug) sigaction(SIGINT,&act,&old); /* go! */ #ifdef USE_THREADS if (nthreads > 1) { int i; threads = malloc(sizeof(pthread_t) * nthreads); for (i = 1; i < nthreads; i++) { pthread_create(threads+i,NULL,mainloop,threads+i); pthread_detach(threads[i]); } } #endif mainloop(NULL); #ifdef USE_SSL if (with_ssl) SSL_CTX_free(ctx); #endif if (logfh) fclose(logfh); if (pidfile) unlink(pidfile); if (debug) fprintf(stderr,"bye...\n"); exit(0); }